@@ -707,6 +707,7 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
707707 return wheel_name
708708
709709
710+ @functools .cache
710711def _find_clang_tidy () -> str | None :
711712 """The clang-tidy executable of the build environment.
712713
@@ -751,20 +752,40 @@ def _warn_clang_tidy_missing() -> None:
751752 )
752753
753754
754- def _get_handwritten_cpp_files (compile_command : Sequence [str ]) -> list [Path ]:
755- """The handwritten C++ sources, not Cython-generated ones, that a compiler command line compiles."""
755+ def _partition_handwritten_cpp_files (args : Sequence [str ]) -> tuple [list [Path ], list [str ]]:
756+ """Split compiler arguments into the handwritten C++ sources and the other arguments.
757+
758+ Cython-generated sources are not handwritten, so they stay among the other
759+ arguments.
760+ """
756761 handwritten_dir = (_PACKAGE_DIR / "_cpp" ).resolve ()
757- files : list [Path ] = []
758- for arg in compile_command :
759- # MSVC passes each C++ source as /Tp<path>, which is removed from the result.
760- path = Path (arg . removeprefix ( "/Tp" ) )
762+ sources : list [Path ] = []
763+ other_args : list [ str ] = []
764+ for arg in args :
765+ path = Path (arg )
761766 if path .suffix == ".cpp" and path .resolve ().is_relative_to (handwritten_dir ):
762- files .append (path )
763- return files
767+ sources .append (path )
768+ else :
769+ other_args .append (arg )
770+ return sources , other_args
771+
772+
773+ def _split_compile_command (compile_command : Sequence [str ]) -> tuple [tuple [str , ...], list [str ]]:
774+ """Split a compiler command line into the compiler and its arguments.
775+
776+ The compiler is the leading words that are not options: a compiler launcher
777+ such as sccache or ccache, if any, and the compiler. MSVC options start with
778+ "/", like paths, so there the compiler is the first word only.
779+ """
780+ n = 1
781+ if sys .platform != "win32" :
782+ while n < len (compile_command ) and not compile_command [n ].startswith ("-" ):
783+ n += 1
784+ return tuple (compile_command [:n ]), list (compile_command [n :])
764785
765786
766787@functools .cache
767- def _get_implicit_include_dirs (compiler : str ) -> list [str ]:
788+ def _get_implicit_include_dirs (compiler : tuple [ str , ...] ) -> list [str ]:
768789 """The include directories that a GCC-style compiler searches without being told.
769790
770791 They are built into the compiler, so they are not on the command line that
@@ -780,7 +801,7 @@ def _get_implicit_include_dirs(compiler: str) -> list[str]:
780801 return []
781802
782803 result = subprocess .run ( # noqa: S603
783- [compiler , "-E" , "-x" , "c++" , "-" , "-v" ],
804+ [* compiler , "-E" , "-x" , "c++" , "-" , "-v" ],
784805 input = "" ,
785806 capture_output = True ,
786807 text = True ,
@@ -795,7 +816,9 @@ def _get_implicit_include_dirs(compiler: str) -> list[str]:
795816 elif in_list and not line .endswith ("(framework directory)" ):
796817 dirs .append (line .strip ())
797818 if result .returncode != 0 or not dirs :
798- raise RuntimeError (f"Failed to query the implicit include directories of { compiler } :\n { result .stderr } " )
819+ raise RuntimeError (
820+ f"Failed to query the implicit include directories of { ' ' .join (compiler )} :\n { result .stderr } "
821+ )
799822 return dirs
800823
801824
@@ -809,22 +832,28 @@ def check_with_clang_tidy(compile_command: Sequence[str]) -> None:
809832 finding into an error. Raises RuntimeError with the findings.
810833
811834 When clang-tidy is not installed, the check is skipped with a prominent
812- warning.
835+ warning, whether or not the command compiles a handwritten source .
813836 """
814- sources = _get_handwritten_cpp_files (compile_command )
815- if not sources :
816- return
817- (source ,) = sources
818837 clang_tidy = _find_clang_tidy ()
819838 if clang_tidy is None :
820839 _warn_clang_tidy_missing ()
821840 return
841+ compiler , args = _split_compile_command (compile_command )
842+ # MSVC passes each C++ source as /Tp<path>.
843+ args = [arg .removeprefix ("/Tp" ) for arg in args ]
844+ sources , args = _partition_handwritten_cpp_files (args )
845+ if not sources :
846+ return
847+ (source ,) = sources
822848 config = Path (__file__ ).resolve ().parent / ".clang-tidy"
823- implicit_includes : list [str ] = []
824- for include_dir in _get_implicit_include_dirs (compile_command [0 ]):
825- implicit_includes .extend (["-isystem" , include_dir ])
849+ if sys .platform == "win32" :
850+ # Without this clang-tidy reads the MSVC options, such as /std:c++17,
851+ # as file names.
852+ args .insert (0 , "--driver-mode=cl" )
853+ for include_dir in _get_implicit_include_dirs (compiler ):
854+ args .extend (["-isystem" , include_dir ])
826855 result = subprocess .run ( # noqa: S603
827- [clang_tidy , f"--config-file={ config } " , "--quiet" , source , "--" , * compile_command , * implicit_includes ],
856+ [clang_tidy , f"--config-file={ config } " , "--quiet" , source , "--" , * args ],
828857 capture_output = True ,
829858 text = True ,
830859 )
0 commit comments