diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 64635311..16147167 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -6,6 +6,7 @@ import sys import os import re +import warnings from .errors import ( CompileError, @@ -824,9 +825,19 @@ def has_function( # noqa: C901 libraries=None, library_dirs=None, ): - """Return a boolean indicating whether funcname is supported on - the current platform. The optional arguments can be used to - augment the compilation environment. + """Return a boolean indicating whether funcname is provided as + a symbol on the current platform. The optional arguments can + be used to augment the compilation environment. + + The libraries argument is a list of flags to be passed to the + linker to make additional symbol definitions available for + linking. + + The includes and include_dirs arguments are deprecated. + Usually, supplying include files with function declarations + will cause function detection to fail even in cases where the + symbol is available for linking. + """ # this can't be included at module scope because it tries to # import math which might not be available at that point - maybe @@ -835,8 +846,12 @@ def has_function( # noqa: C901 if includes is None: includes = [] + else: + warnings.warn("includes is deprecated", DeprecationWarning) if include_dirs is None: include_dirs = [] + else: + warnings.warn("include_dirs is deprecated", DeprecationWarning) if libraries is None: libraries = [] if library_dirs is None: @@ -845,7 +860,24 @@ def has_function( # noqa: C901 f = os.fdopen(fd, "w") try: for incl in includes: - f.write("""#include "%s"\n""" % incl) + f.write("""#include %s\n""" % incl) + if not includes: + # Use "char func(void);" as the prototype to follow + # what autoconf does. This prototype does not match + # any well-known function the compiler might recognize + # as a builtin, so this ends up as a true link test. + # Without a fake prototype, the test would need to + # know the exact argument types, and the has_function + # interface does not provide that level of information. + f.write( + """\ +#ifdef __cplusplus +extern "C" +#endif +char %s(void); +""" + % funcname + ) f.write( """\ int main (int argc, char **argv) { diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index da1879f2..c868a56b 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -53,3 +53,26 @@ def test_set_include_dirs(c_file): # do it again, setting include dirs after any initialization compiler.set_include_dirs([python]) compiler.compile(_make_strs([c_file])) + + +def test_has_function_prototype(): + # Issue https://github.com/pypa/setuptools/issues/3648 + # Test prototype-generating behavior. + + compiler = ccompiler.new_compiler() + + # Every C implementation should have these. + assert compiler.has_function('abort') + assert compiler.has_function('exit') + with pytest.deprecated_call(match='includes is deprecated'): + # abort() is a valid expression with the prototype. + assert compiler.has_function('abort', includes=['']) + with pytest.deprecated_call(match='includes is deprecated'): + # But exit() is not valid with the actual prototype in scope. + assert not compiler.has_function('exit', includes=['']) + # And setuptools_does_not_exist is not declared or defined at all. + assert not compiler.has_function('setuptools_does_not_exist') + with pytest.deprecated_call(match='includes is deprecated'): + assert not compiler.has_function( + 'setuptools_does_not_exist', includes=[''] + ) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 3978c239..a0184424 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -303,4 +303,4 @@ def test_has_function(self): # FileNotFoundError: [Errno 2] No such file or directory: 'a.out' self.cc.output_dir = 'scratch' os.chdir(self.mkdtemp()) - self.cc.has_function('abort', includes=['stdlib.h']) + self.cc.has_function('abort')