@@ -99,6 +99,78 @@ function(declare_mlir_python_sources name)
9999 endif ()
100100endfunction ()
101101
102+ # Function: generate_type_stubs
103+ # Turns on automatic type stub generation (via nanobind's stubgen) for extension modules.
104+ # Arguments:
105+ # MODULE_NAME: The fully-qualified name of the extension module (used for importing in python).
106+ # DEPENDS_TARGET: The dso target corresponding to the extension module
107+ # (e.g., something like StandalonePythonModules.extension._standaloneDialectsNanobind.dso)
108+ # CORE_MLIR_DEPENDS_TARGET: The dso target corresponding to the main/core extension module
109+ # (e.g., something like StandalonePythonModules.extension._mlir.dso)
110+ # OUTPUT_DIR: The root output directory to emit the type stubs into.
111+ # OUTPUTS: List of expected outputs.
112+ # DEPENDS_TARGET_SRC_DEPS: List of cpp sources for extension library (for generating a DEPFILE).
113+ # IMPORT_PATH:
114+ # Outputs:
115+ # NB_STUBGEN_CUSTOM_TARGET: The target corresponding to generation which other targets can depend on.
116+ function (generate_type_stubs)
117+ cmake_parse_arguments (ARG
118+ ""
119+ "MODULE_NAME;DEPENDS_TARGET;CORE_MLIR_DEPENDS_TARGET;OUTPUT_DIR;IMPORT_PATH"
120+ "OUTPUTS;DEPENDS_TARGET_SRC_DEPS"
121+ ${ARGN} )
122+ # for people doing find_package(nanobind)
123+ if (EXISTS ${nanobind_DIR} /../src/stubgen.py)
124+ set (NB_STUBGEN "${nanobind_DIR} /../src/stubgen.py" )
125+ elseif (EXISTS ${nanobind_DIR} /../stubgen.py)
126+ set (NB_STUBGEN "${nanobind_DIR} /../stubgen.py" )
127+ # for people using FetchContent_Declare and FetchContent_MakeAvailable
128+ elseif (EXISTS ${nanobind_SOURCE_DIR} /src/stubgen.py)
129+ set (NB_STUBGEN "${nanobind_SOURCE_DIR} /src/stubgen.py" )
130+ elseif (EXISTS ${nanobind_SOURCE_DIR} /stubgen.py)
131+ set (NB_STUBGEN "${nanobind_SOURCE_DIR} /stubgen.py" )
132+ else ()
133+ message (FATAL_ERROR "generate_type_stubs(): could not locate 'stubgen.py'!" )
134+ endif ()
135+
136+ file (REAL_PATH "${NB_STUBGEN} " NB_STUBGEN)
137+ file (REAL_PATH "${ARG_IMPORT_PATH} " _import_path)
138+ set (_nb_stubgen_cmd
139+ "${Python_EXECUTABLE} "
140+ "${NB_STUBGEN} "
141+ --module
142+ "${ARG_MODULE_NAME} "
143+ -i
144+ "${_import_path} "
145+ --recursive
146+ --include -private
147+ --output -dir
148+ "${ARG_OUTPUT_DIR} "
149+ --quiet )
150+
151+ list (TRANSFORM ARG_OUTPUTS PREPEND "${ARG_OUTPUT_DIR} /" OUTPUT_VARIABLE _generated_type_stubs)
152+ set (_depfile "${ARG_OUTPUT_DIR} /${ARG_MODULE_NAME} .d" )
153+ if ((NOT EXISTS ${_depfile} ) AND ARG_DEPENDS_TARGET_SRC_DEPS)
154+ list (JOIN ARG_DEPENDS_TARGET_SRC_DEPS " " _depfiles)
155+ list (TRANSFORM _generated_type_stubs APPEND ": ${_depfiles} " OUTPUT_VARIABLE _depfiles)
156+ list (JOIN _depfiles "\n " _depfiles)
157+ file (GENERATE OUTPUT "${_depfile} " CONTENT "${_depfiles} " )
158+ endif ()
159+ add_custom_command (
160+ OUTPUT ${_generated_type_stubs}
161+ COMMAND ${_nb_stubgen_cmd}
162+ WORKING_DIRECTORY "${CMAKE_CURRENT_FUNCTION_LIST_DIR} "
163+ DEPENDS
164+ "${ARG_CORE_MLIR_DEPENDS_TARGET} .extension._mlir.dso"
165+ "${ARG_CORE_MLIR_DEPENDS_TARGET} .sources.MLIRPythonSources.Core.Python"
166+ "${ARG_DEPENDS_TARGET} "
167+ DEPFILE "${_depfile} "
168+ )
169+ set (_name "${ARG_CORE_MLIR_DEPENDS_TARGET} .${ARG_DEPENDS_TARGET} .${ARG_MODULE_NAME} .type_stubs" )
170+ add_custom_target ("${_name} " DEPENDS ${_generated_type_stubs} )
171+ set (NB_STUBGEN_CUSTOM_TARGET "${_name} " PARENT_SCOPE)
172+ endfunction ()
173+
102174# Function: declare_mlir_python_extension
103175# Declares a buildable python extension from C++ source files. The built
104176# module is considered a python source file and included as everything else.
@@ -115,11 +187,15 @@ endfunction()
115187# on. These will be collected for all extensions and put into an
116188# aggregate dylib that is linked against.
117189# PYTHON_BINDINGS_LIBRARY: Either pybind11 or nanobind.
190+ # GENERATE_TYPE_STUBS: Either
191+ # 1. OFF (default)
192+ # 2. ON if ${MODULE_NAME}.pyi is the only stub
193+ # 3. A list of generated type stubs expected from stubgen (relative to _mlir_libs).
118194function (declare_mlir_python_extension name )
119195 cmake_parse_arguments (ARG
120196 ""
121197 "ROOT_DIR;MODULE_NAME;ADD_TO_PARENT;PYTHON_BINDINGS_LIBRARY"
122- "SOURCES;PRIVATE_LINK_LIBS;EMBED_CAPI_LINK_LIBS"
198+ "SOURCES;PRIVATE_LINK_LIBS;EMBED_CAPI_LINK_LIBS;GENERATE_TYPE_STUBS "
123199 ${ARGN} )
124200
125201 if (NOT ARG_ROOT_DIR)
@@ -132,15 +208,33 @@ function(declare_mlir_python_extension name)
132208 endif ()
133209
134210 add_library (${name} INTERFACE )
211+
212+ if (NOT ARG_GENERATE_TYPE_STUBS)
213+ set (ARG_GENERATE_TYPE_STUBS OFF )
214+ endif ()
215+ if ("${ARG_GENERATE_TYPE_STUBS} " STREQUAL "ON" )
216+ set (ARG_GENERATE_TYPE_STUBS "${ARG_MODULE_NAME} .pyi" )
217+ endif ()
218+ if (ARG_GENERATE_TYPE_STUBS)
219+ list (TRANSFORM ARG_GENERATE_TYPE_STUBS PREPEND "_mlir_libs/" )
220+ declare_mlir_python_sources(
221+ "${name} .type_stub_gen"
222+ ROOT_DIR "${CMAKE_CURRENT_BINARY_DIR} /type_stubs"
223+ ADD_TO_PARENT "${ARG_ADD_TO_PARENT} "
224+ SOURCES "${ARG_GENERATE_TYPE_STUBS} "
225+ )
226+ endif ()
227+
135228 set_target_properties (${name} PROPERTIES
136229 # Yes: Leading-lowercase property names are load bearing and the recommended
137230 # way to do this: https://gitlab.kitware.com/cmake/cmake/-/issues/19261
138- EXPORT_PROPERTIES "mlir_python_SOURCES_TYPE;mlir_python_EXTENSION_MODULE_NAME;mlir_python_EMBED_CAPI_LINK_LIBS;mlir_python_DEPENDS;mlir_python_BINDINGS_LIBRARY"
231+ EXPORT_PROPERTIES "mlir_python_SOURCES_TYPE;mlir_python_EXTENSION_MODULE_NAME;mlir_python_EMBED_CAPI_LINK_LIBS;mlir_python_DEPENDS;mlir_python_BINDINGS_LIBRARY;mlir_python_GENERATE_TYPE_STUBS "
139232 mlir_python_SOURCES_TYPE extension
140233 mlir_python_EXTENSION_MODULE_NAME "${ARG_MODULE_NAME} "
141234 mlir_python_EMBED_CAPI_LINK_LIBS "${ARG_EMBED_CAPI_LINK_LIBS} "
142235 mlir_python_DEPENDS ""
143236 mlir_python_BINDINGS_LIBRARY "${ARG_PYTHON_BINDINGS_LIBRARY} "
237+ mlir_python_GENERATE_TYPE_STUBS "${ARG_GENERATE_TYPE_STUBS} "
144238 )
145239
146240 # Set the interface source and link_libs properties of the target
@@ -243,6 +337,39 @@ function(add_mlir_python_modules name)
243337 )
244338 add_dependencies (${modules_target} ${_extension_target} )
245339 mlir_python_setup_extension_rpath(${_extension_target} )
340+ # NOTE: `sources_target` (naturally) lists all the sources (it's the INTERFACE
341+ # target defined above in declare_mlir_python_extension). It's also the name of the
342+ # target that gets exported (i.e., is populated as an INTERFACE IMPORTED library in MLIRTargets.cmake).
343+ # This is why all metadata is queried from `sources_target`. On the other hand
344+ # `_extension_target` is the actual dylib target that's built just above with `add_mlir_python_extension`.
345+ # That's why dependencies are in terms of `_extension_target`.
346+ get_target_property (_generate_type_stubs ${sources_target} mlir_python_GENERATE_TYPE_STUBS)
347+ if (_generate_type_stubs)
348+ # TL;DR: all paths here are load bearing and annoyingly coupled. Changing paths here
349+ # (or related code in declare_mlir_python_extension) will break either in-tree or out-of-tree generation.
350+ #
351+ # We remove _mlir_libs here because OUTPUT_DIR already includes it.
352+ # Specifically OUTPUT_DIR already includes it because that's the actual directory
353+ # where we want stubgen to dump the emitted sources. This is load bearing because up above
354+ # (in declare_mlir_python_extension) we prefixed all the paths with _mlir_libs and
355+ # we specified declare_mlir_python_sources with ROOT_DIR "${CMAKE_CURRENT_BINARY_DIR}/type_stubs".
356+ #
357+ # NOTE: INTERFACE_SOURCES is a genex in the build dir ($<BUILD_INTERFACE> $<INSTALL_INTERFACE>)
358+ # which will be evaluated by file(GENERATE ...). In the install dir it's a conventional path
359+ # (see install/lib/cmake/mlir/MLIRTargets.cmake).
360+ get_target_property (_extension_srcs ${sources_target} INTERFACE_SOURCES)
361+ list (TRANSFORM _generate_type_stubs REPLACE "_mlir_libs/" "" )
362+ generate_type_stubs(
363+ MODULE_NAME "_mlir_libs.${_module_name} "
364+ DEPENDS_TARGET ${_extension_target}
365+ CORE_MLIR_DEPENDS_TARGET ${name}
366+ OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR} /type_stubs/_mlir_libs"
367+ OUTPUTS "${_generate_type_stubs} "
368+ DEPENDS_TARGET_SRC_DEPS "${_extension_srcs} "
369+ IMPORT_PATH "${ARG_ROOT_PREFIX} "
370+ )
371+ add_dependencies ("${modules_target} " "${NB_STUBGEN_CUSTOM_TARGET} " )
372+ endif ()
246373 else ()
247374 message (SEND_ERROR "Unrecognized source type '${_source_type} ' for python source target ${sources_target} " )
248375 return ()
@@ -678,26 +805,28 @@ function(add_mlir_python_extension libname extname)
678805 # the super project handle compile options as it wishes.
679806 get_property (NB_LIBRARY_TARGET_NAME TARGET ${libname} PROPERTY LINK_LIBRARIES )
680807 target_compile_options (${NB_LIBRARY_TARGET_NAME}
681- PRIVATE
682- -Wall -Wextra -Wpedantic
683- -Wno-c++98-compat-extra-semi
684- -Wno-cast-qual
685- -Wno-covered-switch-default
686- -Wno-nested-anon-types
687- -Wno-unused-parameter
688- -Wno-zero-length -array
689- ${eh_rtti_enable} )
808+ PRIVATE
809+ -Wall -Wextra -Wpedantic
810+ -Wno-c++98-compat-extra-semi
811+ -Wno-cast-qual
812+ -Wno-covered-switch-default
813+ -Wno-deprecated-literal-operator
814+ -Wno-nested-anon-types
815+ -Wno-unused-parameter
816+ -Wno-zero-length -array
817+ ${eh_rtti_enable} )
690818
691819 target_compile_options (${libname}
692- PRIVATE
693- -Wall -Wextra -Wpedantic
694- -Wno-c++98-compat-extra-semi
695- -Wno-cast-qual
696- -Wno-covered-switch-default
697- -Wno-nested-anon-types
698- -Wno-unused-parameter
699- -Wno-zero-length -array
700- ${eh_rtti_enable} )
820+ PRIVATE
821+ -Wall -Wextra -Wpedantic
822+ -Wno-c++98-compat-extra-semi
823+ -Wno-cast-qual
824+ -Wno-covered-switch-default
825+ -Wno-deprecated-literal-operator
826+ -Wno-nested-anon-types
827+ -Wno-unused-parameter
828+ -Wno-zero-length -array
829+ ${eh_rtti_enable} )
701830 endif ()
702831
703832 if (APPLE )
0 commit comments