Remove hardcoded libpython binaries and add debug step
All checks were successful
build / build-linux (push) Successful in 16s

This commit is contained in:
kdusek
2025-12-07 23:15:18 +01:00
parent 308ce7768e
commit 6a1fe63684
1807 changed files with 172293 additions and 1 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,340 @@
"""distutils.cygwinccompiler
Provides the CygwinCCompiler class, a subclass of UnixCCompiler that
handles the Cygwin port of the GNU C compiler to Windows. It also contains
the Mingw32CCompiler class which handles the mingw32 port of GCC (same as
cygwin in no-cygwin mode).
"""
import copy
import os
import pathlib
import shlex
import sys
import warnings
from subprocess import check_output
from ...errors import (
DistutilsExecError,
DistutilsPlatformError,
)
from ...file_util import write_file
from ...sysconfig import get_config_vars
from ...version import LooseVersion, suppress_known_deprecation
from . import unix
from .errors import (
CompileError,
Error,
)
def get_msvcr():
"""No longer needed, but kept for backward compatibility."""
return []
_runtime_library_dirs_msg = (
"Unable to set runtime library search path on Windows, "
"usually indicated by `runtime_library_dirs` parameter to Extension"
)
class Compiler(unix.Compiler):
"""Handles the Cygwin port of the GNU C compiler to Windows."""
compiler_type = 'cygwin'
obj_extension = ".o"
static_lib_extension = ".a"
shared_lib_extension = ".dll.a"
dylib_lib_extension = ".dll"
static_lib_format = "lib%s%s"
shared_lib_format = "lib%s%s"
dylib_lib_format = "cyg%s%s"
exe_extension = ".exe"
def __init__(self, verbose=False, dry_run=False, force=False):
super().__init__(verbose, dry_run, force)
status, details = check_config_h()
self.debug_print(f"Python's GCC status: {status} (details: {details})")
if status is not CONFIG_H_OK:
self.warn(
"Python's pyconfig.h doesn't seem to support your compiler. "
f"Reason: {details}. "
"Compiling may fail because of undefined preprocessor macros."
)
self.cc, self.cxx = get_config_vars('CC', 'CXX')
# Override 'CC' and 'CXX' environment variables for
# building using MINGW compiler for MSVC python.
self.cc = os.environ.get('CC', self.cc or 'gcc')
self.cxx = os.environ.get('CXX', self.cxx or 'g++')
self.linker_dll = self.cc
self.linker_dll_cxx = self.cxx
shared_option = "-shared"
self.set_executables(
compiler=f'{self.cc} -mcygwin -O -Wall',
compiler_so=f'{self.cc} -mcygwin -mdll -O -Wall',
compiler_cxx=f'{self.cxx} -mcygwin -O -Wall',
compiler_so_cxx=f'{self.cxx} -mcygwin -mdll -O -Wall',
linker_exe=f'{self.cc} -mcygwin',
linker_so=f'{self.linker_dll} -mcygwin {shared_option}',
linker_exe_cxx=f'{self.cxx} -mcygwin',
linker_so_cxx=f'{self.linker_dll_cxx} -mcygwin {shared_option}',
)
self.dll_libraries = get_msvcr()
@property
def gcc_version(self):
# Older numpy depended on this existing to check for ancient
# gcc versions. This doesn't make much sense with clang etc so
# just hardcode to something recent.
# https://github.com/numpy/numpy/pull/20333
warnings.warn(
"gcc_version attribute of CygwinCCompiler is deprecated. "
"Instead of returning actual gcc version a fixed value 11.2.0 is returned.",
DeprecationWarning,
stacklevel=2,
)
with suppress_known_deprecation():
return LooseVersion("11.2.0")
def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
"""Compiles the source by spawning GCC and windres if needed."""
if ext in ('.rc', '.res'):
# gcc needs '.res' and '.rc' compiled to object files !!!
try:
self.spawn(["windres", "-i", src, "-o", obj])
except DistutilsExecError as msg:
raise CompileError(msg)
else: # for other files use the C-compiler
try:
if self.detect_language(src) == 'c++':
self.spawn(
self.compiler_so_cxx
+ cc_args
+ [src, '-o', obj]
+ extra_postargs
)
else:
self.spawn(
self.compiler_so + cc_args + [src, '-o', obj] + extra_postargs
)
except DistutilsExecError as msg:
raise CompileError(msg)
def link(
self,
target_desc,
objects,
output_filename,
output_dir=None,
libraries=None,
library_dirs=None,
runtime_library_dirs=None,
export_symbols=None,
debug=False,
extra_preargs=None,
extra_postargs=None,
build_temp=None,
target_lang=None,
):
"""Link the objects."""
# use separate copies, so we can modify the lists
extra_preargs = copy.copy(extra_preargs or [])
libraries = copy.copy(libraries or [])
objects = copy.copy(objects or [])
if runtime_library_dirs:
self.warn(_runtime_library_dirs_msg)
# Additional libraries
libraries.extend(self.dll_libraries)
# handle export symbols by creating a def-file
# with executables this only works with gcc/ld as linker
if (export_symbols is not None) and (
target_desc != self.EXECUTABLE or self.linker_dll == "gcc"
):
# (The linker doesn't do anything if output is up-to-date.
# So it would probably better to check if we really need this,
# but for this we had to insert some unchanged parts of
# UnixCCompiler, and this is not what we want.)
# we want to put some files in the same directory as the
# object files are, build_temp doesn't help much
# where are the object files
temp_dir = os.path.dirname(objects[0])
# name of dll to give the helper files the same base name
(dll_name, dll_extension) = os.path.splitext(
os.path.basename(output_filename)
)
# generate the filenames for these files
def_file = os.path.join(temp_dir, dll_name + ".def")
# Generate .def file
contents = [f"LIBRARY {os.path.basename(output_filename)}", "EXPORTS"]
contents.extend(export_symbols)
self.execute(write_file, (def_file, contents), f"writing {def_file}")
# next add options for def-file
# for gcc/ld the def-file is specified as any object files
objects.append(def_file)
# end: if ((export_symbols is not None) and
# (target_desc != self.EXECUTABLE or self.linker_dll == "gcc")):
# who wants symbols and a many times larger output file
# should explicitly switch the debug mode on
# otherwise we let ld strip the output file
# (On my machine: 10KiB < stripped_file < ??100KiB
# unstripped_file = stripped_file + XXX KiB
# ( XXX=254 for a typical python extension))
if not debug:
extra_preargs.append("-s")
super().link(
target_desc,
objects,
output_filename,
output_dir,
libraries,
library_dirs,
runtime_library_dirs,
None, # export_symbols, we do this in our def-file
debug,
extra_preargs,
extra_postargs,
build_temp,
target_lang,
)
def runtime_library_dir_option(self, dir):
# cygwin doesn't support rpath. While in theory we could error
# out like MSVC does, code might expect it to work like on Unix, so
# just warn and hope for the best.
self.warn(_runtime_library_dirs_msg)
return []
# -- Miscellaneous methods -----------------------------------------
def _make_out_path(self, output_dir, strip_dir, src_name):
# use normcase to make sure '.rc' is really '.rc' and not '.RC'
norm_src_name = os.path.normcase(src_name)
return super()._make_out_path(output_dir, strip_dir, norm_src_name)
@property
def out_extensions(self):
"""
Add support for rc and res files.
"""
return {
**super().out_extensions,
**{ext: ext + self.obj_extension for ext in ('.res', '.rc')},
}
# the same as cygwin plus some additional parameters
class MinGW32Compiler(Compiler):
"""Handles the Mingw32 port of the GNU C compiler to Windows."""
compiler_type = 'mingw32'
def __init__(self, verbose=False, dry_run=False, force=False):
super().__init__(verbose, dry_run, force)
shared_option = "-shared"
if is_cygwincc(self.cc):
raise Error('Cygwin gcc cannot be used with --compiler=mingw32')
self.set_executables(
compiler=f'{self.cc} -O -Wall',
compiler_so=f'{self.cc} -shared -O -Wall',
compiler_so_cxx=f'{self.cxx} -shared -O -Wall',
compiler_cxx=f'{self.cxx} -O -Wall',
linker_exe=f'{self.cc}',
linker_so=f'{self.linker_dll} {shared_option}',
linker_exe_cxx=f'{self.cxx}',
linker_so_cxx=f'{self.linker_dll_cxx} {shared_option}',
)
def runtime_library_dir_option(self, dir):
raise DistutilsPlatformError(_runtime_library_dirs_msg)
# Because these compilers aren't configured in Python's pyconfig.h file by
# default, we should at least warn the user if he is using an unmodified
# version.
CONFIG_H_OK = "ok"
CONFIG_H_NOTOK = "not ok"
CONFIG_H_UNCERTAIN = "uncertain"
def check_config_h():
"""Check if the current Python installation appears amenable to building
extensions with GCC.
Returns a tuple (status, details), where 'status' is one of the following
constants:
- CONFIG_H_OK: all is well, go ahead and compile
- CONFIG_H_NOTOK: doesn't look good
- CONFIG_H_UNCERTAIN: not sure -- unable to read pyconfig.h
'details' is a human-readable string explaining the situation.
Note there are two ways to conclude "OK": either 'sys.version' contains
the string "GCC" (implying that this Python was built with GCC), or the
installed "pyconfig.h" contains the string "__GNUC__".
"""
# XXX since this function also checks sys.version, it's not strictly a
# "pyconfig.h" check -- should probably be renamed...
from distutils import sysconfig
# if sys.version contains GCC then python was compiled with GCC, and the
# pyconfig.h file should be OK
if "GCC" in sys.version:
return CONFIG_H_OK, "sys.version mentions 'GCC'"
# Clang would also work
if "Clang" in sys.version:
return CONFIG_H_OK, "sys.version mentions 'Clang'"
# let's see if __GNUC__ is mentioned in python.h
fn = sysconfig.get_config_h_filename()
try:
config_h = pathlib.Path(fn).read_text(encoding='utf-8')
except OSError as exc:
return (CONFIG_H_UNCERTAIN, f"couldn't read '{fn}': {exc.strerror}")
else:
substring = '__GNUC__'
if substring in config_h:
code = CONFIG_H_OK
mention_inflected = 'mentions'
else:
code = CONFIG_H_NOTOK
mention_inflected = 'does not mention'
return code, f"{fn!r} {mention_inflected} {substring!r}"
def is_cygwincc(cc):
"""Try to determine if the compiler that would be used is from cygwin."""
out_string = check_output(shlex.split(cc) + ['-dumpmachine'])
return out_string.strip().endswith(b'cygwin')
get_versions = None
"""
A stand-in for the previous get_versions() function to prevent failures
when monkeypatched. See pypa/setuptools#2969.
"""

View File

@@ -0,0 +1,24 @@
class Error(Exception):
"""Some compile/link operation failed."""
class PreprocessError(Error):
"""Failure to preprocess one or more C/C++ files."""
class CompileError(Error):
"""Failure to compile one or more C/C++ source files."""
class LibError(Error):
"""Failure to create a static library from one or more C/C++ object
files."""
class LinkError(Error):
"""Failure to link one or more C/C++ object files into an executable
or shared library file."""
class UnknownFileType(Error):
"""Attempt to process an unknown file type."""

View File

@@ -0,0 +1,614 @@
"""distutils._msvccompiler
Contains MSVCCompiler, an implementation of the abstract CCompiler class
for Microsoft Visual Studio 2015.
This module requires VS 2015 or later.
"""
# Written by Perry Stoll
# hacked by Robin Becker and Thomas Heller to do a better job of
# finding DevStudio (through the registry)
# ported to VS 2005 and VS 2008 by Christian Heimes
# ported to VS 2015 by Steve Dower
from __future__ import annotations
import contextlib
import os
import subprocess
import unittest.mock as mock
import warnings
from collections.abc import Iterable
with contextlib.suppress(ImportError):
import winreg
from itertools import count
from ..._log import log
from ...errors import (
DistutilsExecError,
DistutilsPlatformError,
)
from ...util import get_host_platform, get_platform
from . import base
from .base import gen_lib_options
from .errors import (
CompileError,
LibError,
LinkError,
)
def _find_vc2015():
try:
key = winreg.OpenKeyEx(
winreg.HKEY_LOCAL_MACHINE,
r"Software\Microsoft\VisualStudio\SxS\VC7",
access=winreg.KEY_READ | winreg.KEY_WOW64_32KEY,
)
except OSError:
log.debug("Visual C++ is not registered")
return None, None
best_version = 0
best_dir = None
with key:
for i in count():
try:
v, vc_dir, vt = winreg.EnumValue(key, i)
except OSError:
break
if v and vt == winreg.REG_SZ and os.path.isdir(vc_dir):
try:
version = int(float(v))
except (ValueError, TypeError):
continue
if version >= 14 and version > best_version:
best_version, best_dir = version, vc_dir
return best_version, best_dir
def _find_vc2017():
"""Returns "15, path" based on the result of invoking vswhere.exe
If no install is found, returns "None, None"
The version is returned to avoid unnecessarily changing the function
result. It may be ignored when the path is not None.
If vswhere.exe is not available, by definition, VS 2017 is not
installed.
"""
root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
if not root:
return None, None
variant = 'arm64' if get_platform() == 'win-arm64' else 'x86.x64'
suitable_components = (
f"Microsoft.VisualStudio.Component.VC.Tools.{variant}",
"Microsoft.VisualStudio.Workload.WDExpress",
)
for component in suitable_components:
# Workaround for `-requiresAny` (only available on VS 2017 > 15.6)
with contextlib.suppress(
subprocess.CalledProcessError, OSError, UnicodeDecodeError
):
path = (
subprocess.check_output([
os.path.join(
root, "Microsoft Visual Studio", "Installer", "vswhere.exe"
),
"-latest",
"-prerelease",
"-requires",
component,
"-property",
"installationPath",
"-products",
"*",
])
.decode(encoding="mbcs", errors="strict")
.strip()
)
path = os.path.join(path, "VC", "Auxiliary", "Build")
if os.path.isdir(path):
return 15, path
return None, None # no suitable component found
PLAT_SPEC_TO_RUNTIME = {
'x86': 'x86',
'x86_amd64': 'x64',
'x86_arm': 'arm',
'x86_arm64': 'arm64',
}
def _find_vcvarsall(plat_spec):
# bpo-38597: Removed vcruntime return value
_, best_dir = _find_vc2017()
if not best_dir:
best_version, best_dir = _find_vc2015()
if not best_dir:
log.debug("No suitable Visual C++ version found")
return None, None
vcvarsall = os.path.join(best_dir, "vcvarsall.bat")
if not os.path.isfile(vcvarsall):
log.debug("%s cannot be found", vcvarsall)
return None, None
return vcvarsall, None
def _get_vc_env(plat_spec):
if os.getenv("DISTUTILS_USE_SDK"):
return {key.lower(): value for key, value in os.environ.items()}
vcvarsall, _ = _find_vcvarsall(plat_spec)
if not vcvarsall:
raise DistutilsPlatformError(
'Microsoft Visual C++ 14.0 or greater is required. '
'Get it with "Microsoft C++ Build Tools": '
'https://visualstudio.microsoft.com/visual-cpp-build-tools/'
)
try:
out = subprocess.check_output(
f'cmd /u /c "{vcvarsall}" {plat_spec} && set',
stderr=subprocess.STDOUT,
).decode('utf-16le', errors='replace')
except subprocess.CalledProcessError as exc:
log.error(exc.output)
raise DistutilsPlatformError(f"Error executing {exc.cmd}")
env = {
key.lower(): value
for key, _, value in (line.partition('=') for line in out.splitlines())
if key and value
}
return env
def _find_exe(exe, paths=None):
"""Return path to an MSVC executable program.
Tries to find the program in several places: first, one of the
MSVC program search paths from the registry; next, the directories
in the PATH environment variable. If any of those work, return an
absolute path that is known to exist. If none of them work, just
return the original program name, 'exe'.
"""
if not paths:
paths = os.getenv('path').split(os.pathsep)
for p in paths:
fn = os.path.join(os.path.abspath(p), exe)
if os.path.isfile(fn):
return fn
return exe
_vcvars_names = {
'win32': 'x86',
'win-amd64': 'amd64',
'win-arm32': 'arm',
'win-arm64': 'arm64',
}
def _get_vcvars_spec(host_platform, platform):
"""
Given a host platform and platform, determine the spec for vcvarsall.
Uses the native MSVC host if the host platform would need expensive
emulation for x86.
>>> _get_vcvars_spec('win-arm64', 'win32')
'arm64_x86'
>>> _get_vcvars_spec('win-arm64', 'win-amd64')
'arm64_amd64'
Otherwise, always cross-compile from x86 to work with the
lighter-weight MSVC installs that do not include native 64-bit tools.
>>> _get_vcvars_spec('win32', 'win32')
'x86'
>>> _get_vcvars_spec('win-arm32', 'win-arm32')
'x86_arm'
>>> _get_vcvars_spec('win-amd64', 'win-arm64')
'x86_arm64'
"""
if host_platform != 'win-arm64':
host_platform = 'win32'
vc_hp = _vcvars_names[host_platform]
vc_plat = _vcvars_names[platform]
return vc_hp if vc_hp == vc_plat else f'{vc_hp}_{vc_plat}'
class Compiler(base.Compiler):
"""Concrete class that implements an interface to Microsoft Visual C++,
as defined by the CCompiler abstract class."""
compiler_type = 'msvc'
# Just set this so CCompiler's constructor doesn't barf. We currently
# don't use the 'set_executables()' bureaucracy provided by CCompiler,
# as it really isn't necessary for this sort of single-compiler class.
# Would be nice to have a consistent interface with UnixCCompiler,
# though, so it's worth thinking about.
executables = {}
# Private class data (need to distinguish C from C++ source for compiler)
_c_extensions = ['.c']
_cpp_extensions = ['.cc', '.cpp', '.cxx']
_rc_extensions = ['.rc']
_mc_extensions = ['.mc']
# Needed for the filename generation methods provided by the
# base class, CCompiler.
src_extensions = _c_extensions + _cpp_extensions + _rc_extensions + _mc_extensions
res_extension = '.res'
obj_extension = '.obj'
static_lib_extension = '.lib'
shared_lib_extension = '.dll'
static_lib_format = shared_lib_format = '%s%s'
exe_extension = '.exe'
def __init__(self, verbose=False, dry_run=False, force=False) -> None:
super().__init__(verbose, dry_run, force)
# target platform (.plat_name is consistent with 'bdist')
self.plat_name = None
self.initialized = False
@classmethod
def _configure(cls, vc_env):
"""
Set class-level include/lib dirs.
"""
cls.include_dirs = cls._parse_path(vc_env.get('include', ''))
cls.library_dirs = cls._parse_path(vc_env.get('lib', ''))
@staticmethod
def _parse_path(val):
return [dir.rstrip(os.sep) for dir in val.split(os.pathsep) if dir]
def initialize(self, plat_name: str | None = None) -> None:
# multi-init means we would need to check platform same each time...
assert not self.initialized, "don't init multiple times"
if plat_name is None:
plat_name = get_platform()
# sanity check for platforms to prevent obscure errors later.
if plat_name not in _vcvars_names:
raise DistutilsPlatformError(
f"--plat-name must be one of {tuple(_vcvars_names)}"
)
plat_spec = _get_vcvars_spec(get_host_platform(), plat_name)
vc_env = _get_vc_env(plat_spec)
if not vc_env:
raise DistutilsPlatformError(
"Unable to find a compatible Visual Studio installation."
)
self._configure(vc_env)
self._paths = vc_env.get('path', '')
paths = self._paths.split(os.pathsep)
self.cc = _find_exe("cl.exe", paths)
self.linker = _find_exe("link.exe", paths)
self.lib = _find_exe("lib.exe", paths)
self.rc = _find_exe("rc.exe", paths) # resource compiler
self.mc = _find_exe("mc.exe", paths) # message compiler
self.mt = _find_exe("mt.exe", paths) # message compiler
self.preprocess_options = None
# bpo-38597: Always compile with dynamic linking
# Future releases of Python 3.x will include all past
# versions of vcruntime*.dll for compatibility.
self.compile_options = ['/nologo', '/O2', '/W3', '/GL', '/DNDEBUG', '/MD']
self.compile_options_debug = [
'/nologo',
'/Od',
'/MDd',
'/Zi',
'/W3',
'/D_DEBUG',
]
ldflags = ['/nologo', '/INCREMENTAL:NO', '/LTCG']
ldflags_debug = ['/nologo', '/INCREMENTAL:NO', '/LTCG', '/DEBUG:FULL']
self.ldflags_exe = [*ldflags, '/MANIFEST:EMBED,ID=1']
self.ldflags_exe_debug = [*ldflags_debug, '/MANIFEST:EMBED,ID=1']
self.ldflags_shared = [
*ldflags,
'/DLL',
'/MANIFEST:EMBED,ID=2',
'/MANIFESTUAC:NO',
]
self.ldflags_shared_debug = [
*ldflags_debug,
'/DLL',
'/MANIFEST:EMBED,ID=2',
'/MANIFESTUAC:NO',
]
self.ldflags_static = [*ldflags]
self.ldflags_static_debug = [*ldflags_debug]
self._ldflags = {
(base.Compiler.EXECUTABLE, None): self.ldflags_exe,
(base.Compiler.EXECUTABLE, False): self.ldflags_exe,
(base.Compiler.EXECUTABLE, True): self.ldflags_exe_debug,
(base.Compiler.SHARED_OBJECT, None): self.ldflags_shared,
(base.Compiler.SHARED_OBJECT, False): self.ldflags_shared,
(base.Compiler.SHARED_OBJECT, True): self.ldflags_shared_debug,
(base.Compiler.SHARED_LIBRARY, None): self.ldflags_static,
(base.Compiler.SHARED_LIBRARY, False): self.ldflags_static,
(base.Compiler.SHARED_LIBRARY, True): self.ldflags_static_debug,
}
self.initialized = True
# -- Worker methods ------------------------------------------------
@property
def out_extensions(self) -> dict[str, str]:
return {
**super().out_extensions,
**{
ext: self.res_extension
for ext in self._rc_extensions + self._mc_extensions
},
}
def compile( # noqa: C901
self,
sources,
output_dir=None,
macros=None,
include_dirs=None,
debug=False,
extra_preargs=None,
extra_postargs=None,
depends=None,
):
if not self.initialized:
self.initialize()
compile_info = self._setup_compile(
output_dir, macros, include_dirs, sources, depends, extra_postargs
)
macros, objects, extra_postargs, pp_opts, build = compile_info
compile_opts = extra_preargs or []
compile_opts.append('/c')
if debug:
compile_opts.extend(self.compile_options_debug)
else:
compile_opts.extend(self.compile_options)
add_cpp_opts = False
for obj in objects:
try:
src, ext = build[obj]
except KeyError:
continue
if debug:
# pass the full pathname to MSVC in debug mode,
# this allows the debugger to find the source file
# without asking the user to browse for it
src = os.path.abspath(src)
if ext in self._c_extensions:
input_opt = f"/Tc{src}"
elif ext in self._cpp_extensions:
input_opt = f"/Tp{src}"
add_cpp_opts = True
elif ext in self._rc_extensions:
# compile .RC to .RES file
input_opt = src
output_opt = "/fo" + obj
try:
self.spawn([self.rc] + pp_opts + [output_opt, input_opt])
except DistutilsExecError as msg:
raise CompileError(msg)
continue
elif ext in self._mc_extensions:
# Compile .MC to .RC file to .RES file.
# * '-h dir' specifies the directory for the
# generated include file
# * '-r dir' specifies the target directory of the
# generated RC file and the binary message resource
# it includes
#
# For now (since there are no options to change this),
# we use the source-directory for the include file and
# the build directory for the RC file and message
# resources. This works at least for win32all.
h_dir = os.path.dirname(src)
rc_dir = os.path.dirname(obj)
try:
# first compile .MC to .RC and .H file
self.spawn([self.mc, '-h', h_dir, '-r', rc_dir, src])
base, _ = os.path.splitext(os.path.basename(src))
rc_file = os.path.join(rc_dir, base + '.rc')
# then compile .RC to .RES file
self.spawn([self.rc, "/fo" + obj, rc_file])
except DistutilsExecError as msg:
raise CompileError(msg)
continue
else:
# how to handle this file?
raise CompileError(f"Don't know how to compile {src} to {obj}")
args = [self.cc] + compile_opts + pp_opts
if add_cpp_opts:
args.append('/EHsc')
args.extend((input_opt, "/Fo" + obj))
args.extend(extra_postargs)
try:
self.spawn(args)
except DistutilsExecError as msg:
raise CompileError(msg)
return objects
def create_static_lib(
self,
objects: list[str] | tuple[str, ...],
output_libname: str,
output_dir: str | None = None,
debug: bool = False,
target_lang: str | None = None,
) -> None:
if not self.initialized:
self.initialize()
objects, output_dir = self._fix_object_args(objects, output_dir)
output_filename = self.library_filename(output_libname, output_dir=output_dir)
if self._need_link(objects, output_filename):
lib_args = objects + ['/OUT:' + output_filename]
if debug:
pass # XXX what goes here?
try:
log.debug('Executing "%s" %s', self.lib, ' '.join(lib_args))
self.spawn([self.lib] + lib_args)
except DistutilsExecError as msg:
raise LibError(msg)
else:
log.debug("skipping %s (up-to-date)", output_filename)
def link(
self,
target_desc: str,
objects: list[str] | tuple[str, ...],
output_filename: str,
output_dir: str | None = None,
libraries: list[str] | tuple[str, ...] | None = None,
library_dirs: list[str] | tuple[str, ...] | None = None,
runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
export_symbols: Iterable[str] | None = None,
debug: bool = False,
extra_preargs: list[str] | None = None,
extra_postargs: Iterable[str] | None = None,
build_temp: str | os.PathLike[str] | None = None,
target_lang: str | None = None,
) -> None:
if not self.initialized:
self.initialize()
objects, output_dir = self._fix_object_args(objects, output_dir)
fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)
libraries, library_dirs, runtime_library_dirs = fixed_args
if runtime_library_dirs:
self.warn(
"I don't know what to do with 'runtime_library_dirs': "
+ str(runtime_library_dirs)
)
lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)
if output_dir is not None:
output_filename = os.path.join(output_dir, output_filename)
if self._need_link(objects, output_filename):
ldflags = self._ldflags[target_desc, debug]
export_opts = ["/EXPORT:" + sym for sym in (export_symbols or [])]
ld_args = (
ldflags + lib_opts + export_opts + objects + ['/OUT:' + output_filename]
)
# The MSVC linker generates .lib and .exp files, which cannot be
# suppressed by any linker switches. The .lib files may even be
# needed! Make sure they are generated in the temporary build
# directory. Since they have different names for debug and release
# builds, they can go into the same directory.
build_temp = os.path.dirname(objects[0])
if export_symbols is not None:
(dll_name, dll_ext) = os.path.splitext(
os.path.basename(output_filename)
)
implib_file = os.path.join(build_temp, self.library_filename(dll_name))
ld_args.append('/IMPLIB:' + implib_file)
if extra_preargs:
ld_args[:0] = extra_preargs
if extra_postargs:
ld_args.extend(extra_postargs)
output_dir = os.path.dirname(os.path.abspath(output_filename))
self.mkpath(output_dir)
try:
log.debug('Executing "%s" %s', self.linker, ' '.join(ld_args))
self.spawn([self.linker] + ld_args)
except DistutilsExecError as msg:
raise LinkError(msg)
else:
log.debug("skipping %s (up-to-date)", output_filename)
def spawn(self, cmd):
env = dict(os.environ, PATH=self._paths)
with self._fallback_spawn(cmd, env) as fallback:
return super().spawn(cmd, env=env)
return fallback.value
@contextlib.contextmanager
def _fallback_spawn(self, cmd, env):
"""
Discovered in pypa/distutils#15, some tools monkeypatch the compiler,
so the 'env' kwarg causes a TypeError. Detect this condition and
restore the legacy, unsafe behavior.
"""
bag = type('Bag', (), {})()
try:
yield bag
except TypeError as exc:
if "unexpected keyword argument 'env'" not in str(exc):
raise
else:
return
warnings.warn("Fallback spawn triggered. Please update distutils monkeypatch.")
with mock.patch.dict('os.environ', env):
bag.value = super().spawn(cmd)
# -- Miscellaneous methods -----------------------------------------
# These are all used by the 'gen_lib_options() function, in
# ccompiler.py.
def library_dir_option(self, dir):
return "/LIBPATH:" + dir
def runtime_library_dir_option(self, dir):
raise DistutilsPlatformError(
"don't know how to set runtime library search path for MSVC"
)
def library_option(self, lib):
return self.library_filename(lib)
def find_library_file(self, dirs, lib, debug=False):
# Prefer a debugging library if found (and requested), but deal
# with it if we don't have one.
if debug:
try_names = [lib + "_d", lib]
else:
try_names = [lib]
for dir in dirs:
for name in try_names:
libfile = os.path.join(dir, self.library_filename(name))
if os.path.isfile(libfile):
return libfile
else:
# Oops, didn't find it in *any* of 'dirs'
return None

View File

@@ -0,0 +1,83 @@
import platform
import sysconfig
import textwrap
import pytest
from .. import base
pytestmark = pytest.mark.usefixtures('suppress_path_mangle')
@pytest.fixture
def c_file(tmp_path):
c_file = tmp_path / 'foo.c'
gen_headers = ('Python.h',)
is_windows = platform.system() == "Windows"
plat_headers = ('windows.h',) * is_windows
all_headers = gen_headers + plat_headers
headers = '\n'.join(f'#include <{header}>\n' for header in all_headers)
payload = (
textwrap.dedent(
"""
#headers
void PyInit_foo(void) {}
"""
)
.lstrip()
.replace('#headers', headers)
)
c_file.write_text(payload, encoding='utf-8')
return c_file
def test_set_include_dirs(c_file):
"""
Extensions should build even if set_include_dirs is invoked.
In particular, compiler-specific paths should not be overridden.
"""
compiler = base.new_compiler()
python = sysconfig.get_paths()['include']
compiler.set_include_dirs([python])
compiler.compile([c_file])
# do it again, setting include dirs after any initialization
compiler.set_include_dirs([python])
compiler.compile([c_file])
def test_has_function_prototype():
# Issue https://github.com/pypa/setuptools/issues/3648
# Test prototype-generating behavior.
compiler = base.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 <stdlib.h> prototype.
assert compiler.has_function('abort', includes=['stdlib.h'])
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=['stdlib.h'])
# 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=['stdio.h']
)
def test_include_dirs_after_multiple_compile_calls(c_file):
"""
Calling compile multiple times should not change the include dirs
(regression test for setuptools issue #3591).
"""
compiler = base.new_compiler()
python = sysconfig.get_paths()['include']
compiler.set_include_dirs([python])
compiler.compile([c_file])
assert compiler.include_dirs == [python]
compiler.compile([c_file])
assert compiler.include_dirs == [python]

View File

@@ -0,0 +1,76 @@
"""Tests for distutils.cygwinccompiler."""
import os
import sys
from distutils import sysconfig
from distutils.tests import support
import pytest
from .. import cygwin
@pytest.fixture(autouse=True)
def stuff(request, monkeypatch, distutils_managed_tempdir):
self = request.instance
self.python_h = os.path.join(self.mkdtemp(), 'python.h')
monkeypatch.setattr(sysconfig, 'get_config_h_filename', self._get_config_h_filename)
monkeypatch.setattr(sys, 'version', sys.version)
class TestCygwinCCompiler(support.TempdirManager):
def _get_config_h_filename(self):
return self.python_h
@pytest.mark.skipif('sys.platform != "cygwin"')
@pytest.mark.skipif('not os.path.exists("/usr/lib/libbash.dll.a")')
def test_find_library_file(self):
from distutils.cygwinccompiler import CygwinCCompiler
compiler = CygwinCCompiler()
link_name = "bash"
linkable_file = compiler.find_library_file(["/usr/lib"], link_name)
assert linkable_file is not None
assert os.path.exists(linkable_file)
assert linkable_file == f"/usr/lib/lib{link_name:s}.dll.a"
@pytest.mark.skipif('sys.platform != "cygwin"')
def test_runtime_library_dir_option(self):
from distutils.cygwinccompiler import CygwinCCompiler
compiler = CygwinCCompiler()
assert compiler.runtime_library_dir_option('/foo') == []
def test_check_config_h(self):
# check_config_h looks for "GCC" in sys.version first
# returns CONFIG_H_OK if found
sys.version = (
'2.6.1 (r261:67515, Dec 6 2008, 16:42:21) \n[GCC '
'4.0.1 (Apple Computer, Inc. build 5370)]'
)
assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_OK
# then it tries to see if it can find "__GNUC__" in pyconfig.h
sys.version = 'something without the *CC word'
# if the file doesn't exist it returns CONFIG_H_UNCERTAIN
assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_UNCERTAIN
# if it exists but does not contain __GNUC__, it returns CONFIG_H_NOTOK
self.write_file(self.python_h, 'xxx')
assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_NOTOK
# and CONFIG_H_OK if __GNUC__ is found
self.write_file(self.python_h, 'xxx __GNUC__ xxx')
assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_OK
def test_get_msvcr(self):
assert cygwin.get_msvcr() == []
@pytest.mark.skipif('sys.platform != "cygwin"')
def test_dll_libraries_not_none(self):
from distutils.cygwinccompiler import CygwinCCompiler
compiler = CygwinCCompiler()
assert compiler.dll_libraries is not None

View File

@@ -0,0 +1,48 @@
from distutils import sysconfig
from distutils.errors import DistutilsPlatformError
from distutils.util import is_mingw, split_quoted
import pytest
from .. import cygwin, errors
class TestMinGW32Compiler:
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_compiler_type(self):
compiler = cygwin.MinGW32Compiler()
assert compiler.compiler_type == 'mingw32'
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_set_executables(self, monkeypatch):
monkeypatch.setenv('CC', 'cc')
monkeypatch.setenv('CXX', 'c++')
compiler = cygwin.MinGW32Compiler()
assert compiler.compiler == split_quoted('cc -O -Wall')
assert compiler.compiler_so == split_quoted('cc -shared -O -Wall')
assert compiler.compiler_cxx == split_quoted('c++ -O -Wall')
assert compiler.linker_exe == split_quoted('cc')
assert compiler.linker_so == split_quoted('cc -shared')
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_runtime_library_dir_option(self):
compiler = cygwin.MinGW32Compiler()
with pytest.raises(DistutilsPlatformError):
compiler.runtime_library_dir_option('/usr/lib')
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_cygwincc_error(self, monkeypatch):
monkeypatch.setattr(cygwin, 'is_cygwincc', lambda _: True)
with pytest.raises(errors.Error):
cygwin.MinGW32Compiler()
@pytest.mark.skipif('sys.platform == "cygwin"')
def test_customize_compiler_with_msvc_python(self):
# In case we have an MSVC Python build, but still want to use
# MinGW32Compiler, then customize_compiler() shouldn't fail at least.
# https://github.com/pypa/setuptools/issues/4456
compiler = cygwin.MinGW32Compiler()
sysconfig.customize_compiler(compiler)

View File

@@ -0,0 +1,136 @@
import os
import sys
import sysconfig
import threading
import unittest.mock as mock
from distutils.errors import DistutilsPlatformError
from distutils.tests import support
from distutils.util import get_platform
import pytest
from .. import msvc
needs_winreg = pytest.mark.skipif('not hasattr(msvc, "winreg")')
class Testmsvccompiler(support.TempdirManager):
def test_no_compiler(self, monkeypatch):
# makes sure query_vcvarsall raises
# a DistutilsPlatformError if the compiler
# is not found
def _find_vcvarsall(plat_spec):
return None, None
monkeypatch.setattr(msvc, '_find_vcvarsall', _find_vcvarsall)
with pytest.raises(DistutilsPlatformError):
msvc._get_vc_env(
'wont find this version',
)
@pytest.mark.skipif(
not sysconfig.get_platform().startswith("win"),
reason="Only run test for non-mingw Windows platforms",
)
@pytest.mark.parametrize(
"plat_name, expected",
[
("win-arm64", "win-arm64"),
("win-amd64", "win-amd64"),
(None, get_platform()),
],
)
def test_cross_platform_compilation_paths(self, monkeypatch, plat_name, expected):
"""
Ensure a specified target platform is passed to _get_vcvars_spec.
"""
compiler = msvc.Compiler()
def _get_vcvars_spec(host_platform, platform):
assert platform == expected
monkeypatch.setattr(msvc, '_get_vcvars_spec', _get_vcvars_spec)
compiler.initialize(plat_name)
@needs_winreg
def test_get_vc_env_unicode(self):
test_var = 'ṰḖṤṪ┅ṼẨṜ'
test_value = '₃⁴₅'
# Ensure we don't early exit from _get_vc_env
old_distutils_use_sdk = os.environ.pop('DISTUTILS_USE_SDK', None)
os.environ[test_var] = test_value
try:
env = msvc._get_vc_env('x86')
assert test_var.lower() in env
assert test_value == env[test_var.lower()]
finally:
os.environ.pop(test_var)
if old_distutils_use_sdk:
os.environ['DISTUTILS_USE_SDK'] = old_distutils_use_sdk
@needs_winreg
@pytest.mark.parametrize('ver', (2015, 2017))
def test_get_vc(self, ver):
# This function cannot be mocked, so pass if VC is found
# and skip otherwise.
lookup = getattr(msvc, f'_find_vc{ver}')
expected_version = {2015: 14, 2017: 15}[ver]
version, path = lookup()
if not version:
pytest.skip(f"VS {ver} is not installed")
assert version >= expected_version
assert os.path.isdir(path)
class CheckThread(threading.Thread):
exc_info = None
def run(self):
try:
super().run()
except Exception:
self.exc_info = sys.exc_info()
def __bool__(self):
return not self.exc_info
class TestSpawn:
def test_concurrent_safe(self):
"""
Concurrent calls to spawn should have consistent results.
"""
compiler = msvc.Compiler()
compiler._paths = "expected"
inner_cmd = 'import os; assert os.environ["PATH"] == "expected"'
command = [sys.executable, '-c', inner_cmd]
threads = [
CheckThread(target=compiler.spawn, args=[command]) for n in range(100)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert all(threads)
def test_concurrent_safe_fallback(self):
"""
If CCompiler.spawn has been monkey-patched without support
for an env, it should still execute.
"""
from distutils import ccompiler
compiler = msvc.Compiler()
compiler._paths = "expected"
def CCompiler_spawn(self, cmd):
"A spawn without an env argument."
assert os.environ["PATH"] == "expected"
with mock.patch.object(ccompiler.CCompiler, 'spawn', CCompiler_spawn):
compiler.spawn(["n/a"])
assert os.environ.get("PATH") != "expected"

View File

@@ -0,0 +1,413 @@
"""Tests for distutils.unixccompiler."""
import os
import sys
import unittest.mock as mock
from distutils import sysconfig
from distutils.compat import consolidate_linker_args
from distutils.errors import DistutilsPlatformError
from distutils.tests import support
from distutils.tests.compat.py39 import EnvironmentVarGuard
from distutils.util import _clear_cached_macosx_ver
import pytest
from .. import unix
@pytest.fixture(autouse=True)
def save_values(monkeypatch):
monkeypatch.setattr(sys, 'platform', sys.platform)
monkeypatch.setattr(sysconfig, 'get_config_var', sysconfig.get_config_var)
monkeypatch.setattr(sysconfig, 'get_config_vars', sysconfig.get_config_vars)
@pytest.fixture(autouse=True)
def compiler_wrapper(request):
class CompilerWrapper(unix.Compiler):
def rpath_foo(self):
return self.runtime_library_dir_option('/foo')
request.instance.cc = CompilerWrapper()
class TestUnixCCompiler(support.TempdirManager):
@pytest.mark.skipif('platform.system == "Windows"')
def test_runtime_libdir_option(self): # noqa: C901
# Issue #5900; GitHub Issue #37
#
# Ensure RUNPATH is added to extension modules with RPATH if
# GNU ld is used
# darwin
sys.platform = 'darwin'
darwin_ver_var = 'MACOSX_DEPLOYMENT_TARGET'
darwin_rpath_flag = '-Wl,-rpath,/foo'
darwin_lib_flag = '-L/foo'
# (macOS version from syscfg, macOS version from env var) -> flag
# Version value of None generates two tests: as None and as empty string
# Expected flag value of None means an mismatch exception is expected
darwin_test_cases = [
((None, None), darwin_lib_flag),
((None, '11'), darwin_rpath_flag),
(('10', None), darwin_lib_flag),
(('10.3', None), darwin_lib_flag),
(('10.3.1', None), darwin_lib_flag),
(('10.5', None), darwin_rpath_flag),
(('10.5.1', None), darwin_rpath_flag),
(('10.3', '10.3'), darwin_lib_flag),
(('10.3', '10.5'), darwin_rpath_flag),
(('10.5', '10.3'), darwin_lib_flag),
(('10.5', '11'), darwin_rpath_flag),
(('10.4', '10'), None),
]
def make_darwin_gcv(syscfg_macosx_ver):
def gcv(var):
if var == darwin_ver_var:
return syscfg_macosx_ver
return "xxx"
return gcv
def do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag):
env = os.environ
msg = f"macOS version = (sysconfig={syscfg_macosx_ver!r}, env={env_macosx_ver!r})"
# Save
old_gcv = sysconfig.get_config_var
old_env_macosx_ver = env.get(darwin_ver_var)
# Setup environment
_clear_cached_macosx_ver()
sysconfig.get_config_var = make_darwin_gcv(syscfg_macosx_ver)
if env_macosx_ver is not None:
env[darwin_ver_var] = env_macosx_ver
elif darwin_ver_var in env:
env.pop(darwin_ver_var)
# Run the test
if expected_flag is not None:
assert self.cc.rpath_foo() == expected_flag, msg
else:
with pytest.raises(
DistutilsPlatformError, match=darwin_ver_var + r' mismatch'
):
self.cc.rpath_foo()
# Restore
if old_env_macosx_ver is not None:
env[darwin_ver_var] = old_env_macosx_ver
elif darwin_ver_var in env:
env.pop(darwin_ver_var)
sysconfig.get_config_var = old_gcv
_clear_cached_macosx_ver()
for macosx_vers, expected_flag in darwin_test_cases:
syscfg_macosx_ver, env_macosx_ver = macosx_vers
do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag)
# Bonus test cases with None interpreted as empty string
if syscfg_macosx_ver is None:
do_darwin_test("", env_macosx_ver, expected_flag)
if env_macosx_ver is None:
do_darwin_test(syscfg_macosx_ver, "", expected_flag)
if syscfg_macosx_ver is None and env_macosx_ver is None:
do_darwin_test("", "", expected_flag)
old_gcv = sysconfig.get_config_var
# hp-ux
sys.platform = 'hp-ux'
def gcv(v):
return 'xxx'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == ['+s', '-L/foo']
def gcv(v):
return 'gcc'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == ['-Wl,+s', '-L/foo']
def gcv(v):
return 'g++'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == ['-Wl,+s', '-L/foo']
sysconfig.get_config_var = old_gcv
# GCC GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'gcc'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
def gcv(v):
if v == 'CC':
return 'gcc -pthread -B /bar'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
# GCC non-GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'gcc'
elif v == 'GNULD':
return 'no'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == '-Wl,-R/foo'
# GCC GNULD with fully qualified configuration prefix
# see #7617
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'x86_64-pc-linux-gnu-gcc-4.4.2'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
# non-GCC GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'cc'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
# non-GCC non-GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'cc'
elif v == 'GNULD':
return 'no'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == '-Wl,-R/foo'
@pytest.mark.skipif('platform.system == "Windows"')
def test_cc_overrides_ldshared(self):
# Issue #18080:
# ensure that setting CC env variable also changes default linker
def gcv(v):
if v == 'LDSHARED':
return 'gcc-4.2 -bundle -undefined dynamic_lookup '
return 'gcc-4.2'
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig()
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with EnvironmentVarGuard() as env:
env['CC'] = 'my_cc'
del env['LDSHARED']
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so[0] == 'my_cc'
@pytest.mark.skipif('platform.system == "Windows"')
def test_cxx_commands_used_are_correct(self):
def gcv(v):
if v == 'LDSHARED':
return 'ccache gcc-4.2 -bundle -undefined dynamic_lookup'
elif v == 'LDCXXSHARED':
return 'ccache g++-4.2 -bundle -undefined dynamic_lookup'
elif v == 'CXX':
return 'ccache g++-4.2'
elif v == 'CC':
return 'ccache gcc-4.2'
return ''
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig() # pragma: no cover
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with (
mock.patch.object(self.cc, 'spawn', return_value=None) as mock_spawn,
mock.patch.object(self.cc, '_need_link', return_value=True),
mock.patch.object(self.cc, 'mkpath', return_value=None),
EnvironmentVarGuard() as env,
):
# override environment overrides in case they're specified by CI
del env['CXX']
del env['LDCXXSHARED']
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so_cxx[0:2] == ['ccache', 'g++-4.2']
assert self.cc.linker_exe_cxx[0:2] == ['ccache', 'g++-4.2']
self.cc.link(None, [], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = ['ccache', 'g++-4.2', '-bundle', '-undefined', 'dynamic_lookup']
assert call_args[:5] == expected
self.cc.link_executable([], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = ['ccache', 'g++-4.2', '-o', self.cc.executable_filename('a.out')]
assert call_args[:4] == expected
env['LDCXXSHARED'] = 'wrapper g++-4.2 -bundle -undefined dynamic_lookup'
env['CXX'] = 'wrapper g++-4.2'
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so_cxx[0:2] == ['wrapper', 'g++-4.2']
assert self.cc.linker_exe_cxx[0:2] == ['wrapper', 'g++-4.2']
self.cc.link(None, [], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = ['wrapper', 'g++-4.2', '-bundle', '-undefined', 'dynamic_lookup']
assert call_args[:5] == expected
self.cc.link_executable([], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = [
'wrapper',
'g++-4.2',
'-o',
self.cc.executable_filename('a.out'),
]
assert call_args[:4] == expected
@pytest.mark.skipif('platform.system == "Windows"')
@pytest.mark.usefixtures('disable_macos_customization')
def test_cc_overrides_ldshared_for_cxx_correctly(self):
"""
Ensure that setting CC env variable also changes default linker
correctly when building C++ extensions.
pypa/distutils#126
"""
def gcv(v):
if v == 'LDSHARED':
return 'gcc-4.2 -bundle -undefined dynamic_lookup '
elif v == 'LDCXXSHARED':
return 'g++-4.2 -bundle -undefined dynamic_lookup '
elif v == 'CXX':
return 'g++-4.2'
elif v == 'CC':
return 'gcc-4.2'
return ''
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig()
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with (
mock.patch.object(self.cc, 'spawn', return_value=None) as mock_spawn,
mock.patch.object(self.cc, '_need_link', return_value=True),
mock.patch.object(self.cc, 'mkpath', return_value=None),
EnvironmentVarGuard() as env,
):
env['CC'] = 'ccache my_cc'
env['CXX'] = 'my_cxx'
del env['LDSHARED']
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so[0:2] == ['ccache', 'my_cc']
self.cc.link(None, [], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = ['my_cxx', '-bundle', '-undefined', 'dynamic_lookup']
assert call_args[:4] == expected
@pytest.mark.skipif('platform.system == "Windows"')
def test_explicit_ldshared(self):
# Issue #18080:
# ensure that setting CC env variable does not change
# explicit LDSHARED setting for linker
def gcv(v):
if v == 'LDSHARED':
return 'gcc-4.2 -bundle -undefined dynamic_lookup '
return 'gcc-4.2'
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig()
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with EnvironmentVarGuard() as env:
env['CC'] = 'my_cc'
env['LDSHARED'] = 'my_ld -bundle -dynamic'
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so[0] == 'my_ld'
def test_has_function(self):
# Issue https://github.com/pypa/distutils/issues/64:
# ensure that setting output_dir does not raise
# FileNotFoundError: [Errno 2] No such file or directory: 'a.out'
self.cc.output_dir = 'scratch'
os.chdir(self.mkdtemp())
self.cc.has_function('abort')
def test_find_library_file(self, monkeypatch):
compiler = unix.Compiler()
compiler._library_root = lambda dir: dir
monkeypatch.setattr(os.path, 'exists', lambda d: 'existing' in d)
libname = 'libabc.dylib' if sys.platform != 'cygwin' else 'cygabc.dll'
dirs = ('/foo/bar/missing', '/foo/bar/existing')
assert (
compiler.find_library_file(dirs, 'abc').replace('\\', '/')
== f'/foo/bar/existing/{libname}'
)
assert (
compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/')
== f'/foo/bar/existing/{libname}'
)
monkeypatch.setattr(
os.path,
'exists',
lambda d: 'existing' in d and '.a' in d and '.dll.a' not in d,
)
assert (
compiler.find_library_file(dirs, 'abc').replace('\\', '/')
== '/foo/bar/existing/libabc.a'
)
assert (
compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/')
== '/foo/bar/existing/libabc.a'
)

View File

@@ -0,0 +1,422 @@
"""distutils.unixccompiler
Contains the UnixCCompiler class, a subclass of CCompiler that handles
the "typical" Unix-style command-line C compiler:
* macros defined with -Dname[=value]
* macros undefined with -Uname
* include search directories specified with -Idir
* libraries specified with -lllib
* library search directories specified with -Ldir
* compile handled by 'cc' (or similar) executable with -c option:
compiles .c to .o
* link static library handled by 'ar' command (possibly with 'ranlib')
* link shared library handled by 'cc -shared'
"""
from __future__ import annotations
import itertools
import os
import re
import shlex
import sys
from collections.abc import Iterable
from ... import sysconfig
from ..._log import log
from ..._macos_compat import compiler_fixup
from ..._modified import newer
from ...compat import consolidate_linker_args
from ...errors import DistutilsExecError
from . import base
from .base import _Macro, gen_lib_options, gen_preprocess_options
from .errors import (
CompileError,
LibError,
LinkError,
)
# XXX Things not currently handled:
# * optimization/debug/warning flags; we just use whatever's in Python's
# Makefile and live with it. Is this adequate? If not, we might
# have to have a bunch of subclasses GNUCCompiler, SGICCompiler,
# SunCCompiler, and I suspect down that road lies madness.
# * even if we don't know a warning flag from an optimization flag,
# we need some way for outsiders to feed preprocessor/compiler/linker
# flags in to us -- eg. a sysadmin might want to mandate certain flags
# via a site config file, or a user might want to set something for
# compiling this module distribution only via the setup.py command
# line, whatever. As long as these options come from something on the
# current system, they can be as system-dependent as they like, and we
# should just happily stuff them into the preprocessor/compiler/linker
# options and carry on.
def _split_env(cmd):
"""
For macOS, split command into 'env' portion (if any)
and the rest of the linker command.
>>> _split_env(['a', 'b', 'c'])
([], ['a', 'b', 'c'])
>>> _split_env(['/usr/bin/env', 'A=3', 'gcc'])
(['/usr/bin/env', 'A=3'], ['gcc'])
"""
pivot = 0
if os.path.basename(cmd[0]) == "env":
pivot = 1
while '=' in cmd[pivot]:
pivot += 1
return cmd[:pivot], cmd[pivot:]
def _split_aix(cmd):
"""
AIX platforms prefix the compiler with the ld_so_aix
script, so split that from the linker command.
>>> _split_aix(['a', 'b', 'c'])
([], ['a', 'b', 'c'])
>>> _split_aix(['/bin/foo/ld_so_aix', 'gcc'])
(['/bin/foo/ld_so_aix'], ['gcc'])
"""
pivot = os.path.basename(cmd[0]) == 'ld_so_aix'
return cmd[:pivot], cmd[pivot:]
def _linker_params(linker_cmd, compiler_cmd):
"""
The linker command usually begins with the compiler
command (possibly multiple elements), followed by zero or more
params for shared library building.
If the LDSHARED env variable overrides the linker command,
however, the commands may not match.
Return the best guess of the linker parameters by stripping
the linker command. If the compiler command does not
match the linker command, assume the linker command is
just the first element.
>>> _linker_params('gcc foo bar'.split(), ['gcc'])
['foo', 'bar']
>>> _linker_params('gcc foo bar'.split(), ['other'])
['foo', 'bar']
>>> _linker_params('ccache gcc foo bar'.split(), 'ccache gcc'.split())
['foo', 'bar']
>>> _linker_params(['gcc'], ['gcc'])
[]
"""
c_len = len(compiler_cmd)
pivot = c_len if linker_cmd[:c_len] == compiler_cmd else 1
return linker_cmd[pivot:]
class Compiler(base.Compiler):
compiler_type = 'unix'
# These are used by CCompiler in two places: the constructor sets
# instance attributes 'preprocessor', 'compiler', etc. from them, and
# 'set_executable()' allows any of these to be set. The defaults here
# are pretty generic; they will probably have to be set by an outsider
# (eg. using information discovered by the sysconfig about building
# Python extensions).
executables = {
'preprocessor': None,
'compiler': ["cc"],
'compiler_so': ["cc"],
'compiler_cxx': ["c++"],
'compiler_so_cxx': ["c++"],
'linker_so': ["cc", "-shared"],
'linker_so_cxx': ["c++", "-shared"],
'linker_exe': ["cc"],
'linker_exe_cxx': ["c++", "-shared"],
'archiver': ["ar", "-cr"],
'ranlib': None,
}
if sys.platform[:6] == "darwin":
executables['ranlib'] = ["ranlib"]
# Needed for the filename generation methods provided by the base
# class, CCompiler. NB. whoever instantiates/uses a particular
# UnixCCompiler instance should set 'shared_lib_ext' -- we set a
# reasonable common default here, but it's not necessarily used on all
# Unices!
src_extensions = [".c", ".C", ".cc", ".cxx", ".cpp", ".m"]
obj_extension = ".o"
static_lib_extension = ".a"
shared_lib_extension = ".so"
dylib_lib_extension = ".dylib"
xcode_stub_lib_extension = ".tbd"
static_lib_format = shared_lib_format = dylib_lib_format = "lib%s%s"
xcode_stub_lib_format = dylib_lib_format
if sys.platform == "cygwin":
exe_extension = ".exe"
shared_lib_extension = ".dll.a"
dylib_lib_extension = ".dll"
dylib_lib_format = "cyg%s%s"
def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs):
"""Remove standard library path from rpath"""
libraries, library_dirs, runtime_library_dirs = super()._fix_lib_args(
libraries, library_dirs, runtime_library_dirs
)
libdir = sysconfig.get_config_var('LIBDIR')
if (
runtime_library_dirs
and libdir.startswith("/usr/lib")
and (libdir in runtime_library_dirs)
):
runtime_library_dirs.remove(libdir)
return libraries, library_dirs, runtime_library_dirs
def preprocess(
self,
source: str | os.PathLike[str],
output_file: str | os.PathLike[str] | None = None,
macros: list[_Macro] | None = None,
include_dirs: list[str] | tuple[str, ...] | None = None,
extra_preargs: list[str] | None = None,
extra_postargs: Iterable[str] | None = None,
):
fixed_args = self._fix_compile_args(None, macros, include_dirs)
ignore, macros, include_dirs = fixed_args
pp_opts = gen_preprocess_options(macros, include_dirs)
pp_args = self.preprocessor + pp_opts
if output_file:
pp_args.extend(['-o', output_file])
if extra_preargs:
pp_args[:0] = extra_preargs
if extra_postargs:
pp_args.extend(extra_postargs)
pp_args.append(source)
# reasons to preprocess:
# - force is indicated
# - output is directed to stdout
# - source file is newer than the target
preprocess = self.force or output_file is None or newer(source, output_file)
if not preprocess:
return
if output_file:
self.mkpath(os.path.dirname(output_file))
try:
self.spawn(pp_args)
except DistutilsExecError as msg:
raise CompileError(msg)
def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
compiler_so = compiler_fixup(self.compiler_so, cc_args + extra_postargs)
compiler_so_cxx = compiler_fixup(self.compiler_so_cxx, cc_args + extra_postargs)
try:
if self.detect_language(src) == 'c++':
self.spawn(
compiler_so_cxx + cc_args + [src, '-o', obj] + extra_postargs
)
else:
self.spawn(compiler_so + cc_args + [src, '-o', obj] + extra_postargs)
except DistutilsExecError as msg:
raise CompileError(msg)
def create_static_lib(
self, objects, output_libname, output_dir=None, debug=False, target_lang=None
):
objects, output_dir = self._fix_object_args(objects, output_dir)
output_filename = self.library_filename(output_libname, output_dir=output_dir)
if self._need_link(objects, output_filename):
self.mkpath(os.path.dirname(output_filename))
self.spawn(self.archiver + [output_filename] + objects + self.objects)
# Not many Unices required ranlib anymore -- SunOS 4.x is, I
# think the only major Unix that does. Maybe we need some
# platform intelligence here to skip ranlib if it's not
# needed -- or maybe Python's configure script took care of
# it for us, hence the check for leading colon.
if self.ranlib:
try:
self.spawn(self.ranlib + [output_filename])
except DistutilsExecError as msg:
raise LibError(msg)
else:
log.debug("skipping %s (up-to-date)", output_filename)
def link(
self,
target_desc,
objects: list[str] | tuple[str, ...],
output_filename,
output_dir: str | None = None,
libraries: list[str] | tuple[str, ...] | None = None,
library_dirs: list[str] | tuple[str, ...] | None = None,
runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
export_symbols=None,
debug=False,
extra_preargs=None,
extra_postargs=None,
build_temp=None,
target_lang=None,
):
objects, output_dir = self._fix_object_args(objects, output_dir)
fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)
libraries, library_dirs, runtime_library_dirs = fixed_args
lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)
if not isinstance(output_dir, (str, type(None))):
raise TypeError("'output_dir' must be a string or None")
if output_dir is not None:
output_filename = os.path.join(output_dir, output_filename)
if self._need_link(objects, output_filename):
ld_args = objects + self.objects + lib_opts + ['-o', output_filename]
if debug:
ld_args[:0] = ['-g']
if extra_preargs:
ld_args[:0] = extra_preargs
if extra_postargs:
ld_args.extend(extra_postargs)
self.mkpath(os.path.dirname(output_filename))
try:
# Select a linker based on context: linker_exe when
# building an executable or linker_so (with shared options)
# when building a shared library.
building_exe = target_desc == base.Compiler.EXECUTABLE
target_cxx = target_lang == "c++"
linker = (
(self.linker_exe_cxx if target_cxx else self.linker_exe)
if building_exe
else (self.linker_so_cxx if target_cxx else self.linker_so)
)[:]
if target_cxx and self.compiler_cxx:
env, linker_ne = _split_env(linker)
aix, linker_na = _split_aix(linker_ne)
_, compiler_cxx_ne = _split_env(self.compiler_cxx)
_, linker_exe_ne = _split_env(self.linker_exe_cxx)
params = _linker_params(linker_na, linker_exe_ne)
linker = env + aix + compiler_cxx_ne + params
linker = compiler_fixup(linker, ld_args)
self.spawn(linker + ld_args)
except DistutilsExecError as msg:
raise LinkError(msg)
else:
log.debug("skipping %s (up-to-date)", output_filename)
# -- Miscellaneous methods -----------------------------------------
# These are all used by the 'gen_lib_options() function, in
# ccompiler.py.
def library_dir_option(self, dir):
return "-L" + dir
def _is_gcc(self):
cc_var = sysconfig.get_config_var("CC")
compiler = os.path.basename(shlex.split(cc_var)[0])
return "gcc" in compiler or "g++" in compiler
def runtime_library_dir_option(self, dir: str) -> str | list[str]: # type: ignore[override] # Fixed in pypa/distutils#339
# XXX Hackish, at the very least. See Python bug #445902:
# https://bugs.python.org/issue445902
# Linkers on different platforms need different options to
# specify that directories need to be added to the list of
# directories searched for dependencies when a dynamic library
# is sought. GCC on GNU systems (Linux, FreeBSD, ...) has to
# be told to pass the -R option through to the linker, whereas
# other compilers and gcc on other systems just know this.
# Other compilers may need something slightly different. At
# this time, there's no way to determine this information from
# the configuration data stored in the Python installation, so
# we use this hack.
if sys.platform[:6] == "darwin":
from distutils.util import get_macosx_target_ver, split_version
macosx_target_ver = get_macosx_target_ver()
if macosx_target_ver and split_version(macosx_target_ver) >= [10, 5]:
return "-Wl,-rpath," + dir
else: # no support for -rpath on earlier macOS versions
return "-L" + dir
elif sys.platform[:7] == "freebsd":
return "-Wl,-rpath=" + dir
elif sys.platform[:5] == "hp-ux":
return [
"-Wl,+s" if self._is_gcc() else "+s",
"-L" + dir,
]
# For all compilers, `-Wl` is the presumed way to pass a
# compiler option to the linker
if sysconfig.get_config_var("GNULD") == "yes":
return consolidate_linker_args([
# Force RUNPATH instead of RPATH
"-Wl,--enable-new-dtags",
"-Wl,-rpath," + dir,
])
else:
return "-Wl,-R" + dir
def library_option(self, lib):
return "-l" + lib
@staticmethod
def _library_root(dir):
"""
macOS users can specify an alternate SDK using'-isysroot'.
Calculate the SDK root if it is specified.
Note that, as of Xcode 7, Apple SDKs may contain textual stub
libraries with .tbd extensions rather than the normal .dylib
shared libraries installed in /. The Apple compiler tool
chain handles this transparently but it can cause problems
for programs that are being built with an SDK and searching
for specific libraries. Callers of find_library_file need to
keep in mind that the base filename of the returned SDK library
file might have a different extension from that of the library
file installed on the running system, for example:
/Applications/Xcode.app/Contents/Developer/Platforms/
MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/
usr/lib/libedit.tbd
vs
/usr/lib/libedit.dylib
"""
cflags = sysconfig.get_config_var('CFLAGS')
match = re.search(r'-isysroot\s*(\S+)', cflags)
apply_root = (
sys.platform == 'darwin'
and match
and (
dir.startswith('/System/')
or (dir.startswith('/usr/') and not dir.startswith('/usr/local/'))
)
)
return os.path.join(match.group(1), dir[1:]) if apply_root else dir
def find_library_file(self, dirs, lib, debug=False):
"""
Second-guess the linker with not much hard
data to go on: GCC seems to prefer the shared library, so
assume that *all* Unix C compilers do,
ignoring even GCC's "-static" option.
"""
lib_names = (
self.library_filename(lib, lib_type=type)
for type in 'dylib xcode_stub shared static'.split()
)
roots = map(self._library_root, dirs)
searched = itertools.starmap(os.path.join, itertools.product(roots, lib_names))
found = filter(os.path.exists, searched)
# Return None if it could not be found in any dir.
return next(found, None)

View File

@@ -0,0 +1,230 @@
"""distutils.zosccompiler
Contains the selection of the c & c++ compilers on z/OS. There are several
different c compilers on z/OS, all of them are optional, so the correct
one needs to be chosen based on the users input. This is compatible with
the following compilers:
IBM C/C++ For Open Enterprise Languages on z/OS 2.0
IBM Open XL C/C++ 1.1 for z/OS
IBM XL C/C++ V2.4.1 for z/OS 2.4 and 2.5
IBM z/OS XL C/C++
"""
import os
from ... import sysconfig
from ...errors import DistutilsExecError
from . import unix
from .errors import CompileError
_cc_args = {
'ibm-openxl': [
'-m64',
'-fvisibility=default',
'-fzos-le-char-mode=ascii',
'-fno-short-enums',
],
'ibm-xlclang': [
'-q64',
'-qexportall',
'-qascii',
'-qstrict',
'-qnocsect',
'-Wa,asa,goff',
'-Wa,xplink',
'-qgonumber',
'-qenum=int',
'-Wc,DLL',
],
'ibm-xlc': [
'-q64',
'-qexportall',
'-qascii',
'-qstrict',
'-qnocsect',
'-Wa,asa,goff',
'-Wa,xplink',
'-qgonumber',
'-qenum=int',
'-Wc,DLL',
'-qlanglvl=extc99',
],
}
_cxx_args = {
'ibm-openxl': [
'-m64',
'-fvisibility=default',
'-fzos-le-char-mode=ascii',
'-fno-short-enums',
],
'ibm-xlclang': [
'-q64',
'-qexportall',
'-qascii',
'-qstrict',
'-qnocsect',
'-Wa,asa,goff',
'-Wa,xplink',
'-qgonumber',
'-qenum=int',
'-Wc,DLL',
],
'ibm-xlc': [
'-q64',
'-qexportall',
'-qascii',
'-qstrict',
'-qnocsect',
'-Wa,asa,goff',
'-Wa,xplink',
'-qgonumber',
'-qenum=int',
'-Wc,DLL',
'-qlanglvl=extended0x',
],
}
_asm_args = {
'ibm-openxl': ['-fasm', '-fno-integrated-as', '-Wa,--ASA', '-Wa,--GOFF'],
'ibm-xlclang': [],
'ibm-xlc': [],
}
_ld_args = {
'ibm-openxl': [],
'ibm-xlclang': ['-Wl,dll', '-q64'],
'ibm-xlc': ['-Wl,dll', '-q64'],
}
# Python on z/OS is built with no compiler specific options in it's CFLAGS.
# But each compiler requires it's own specific options to build successfully,
# though some of the options are common between them
class Compiler(unix.Compiler):
src_extensions = ['.c', '.C', '.cc', '.cxx', '.cpp', '.m', '.s']
_cpp_extensions = ['.cc', '.cpp', '.cxx', '.C']
_asm_extensions = ['.s']
def _get_zos_compiler_name(self):
zos_compiler_names = [
os.path.basename(binary)
for envvar in ('CC', 'CXX', 'LDSHARED')
if (binary := os.environ.get(envvar, None))
]
if len(zos_compiler_names) == 0:
return 'ibm-openxl'
zos_compilers = {}
for compiler in (
'ibm-clang',
'ibm-clang64',
'ibm-clang++',
'ibm-clang++64',
'clang',
'clang++',
'clang-14',
):
zos_compilers[compiler] = 'ibm-openxl'
for compiler in ('xlclang', 'xlclang++', 'njsc', 'njsc++'):
zos_compilers[compiler] = 'ibm-xlclang'
for compiler in ('xlc', 'xlC', 'xlc++'):
zos_compilers[compiler] = 'ibm-xlc'
return zos_compilers.get(zos_compiler_names[0], 'ibm-openxl')
def __init__(self, verbose=False, dry_run=False, force=False):
super().__init__(verbose, dry_run, force)
self.zos_compiler = self._get_zos_compiler_name()
sysconfig.customize_compiler(self)
def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
local_args = []
if ext in self._cpp_extensions:
compiler = self.compiler_cxx
local_args.extend(_cxx_args[self.zos_compiler])
elif ext in self._asm_extensions:
compiler = self.compiler_so
local_args.extend(_cc_args[self.zos_compiler])
local_args.extend(_asm_args[self.zos_compiler])
else:
compiler = self.compiler_so
local_args.extend(_cc_args[self.zos_compiler])
local_args.extend(cc_args)
try:
self.spawn(compiler + local_args + [src, '-o', obj] + extra_postargs)
except DistutilsExecError as msg:
raise CompileError(msg)
def runtime_library_dir_option(self, dir):
return '-L' + dir
def link(
self,
target_desc,
objects,
output_filename,
output_dir=None,
libraries=None,
library_dirs=None,
runtime_library_dirs=None,
export_symbols=None,
debug=False,
extra_preargs=None,
extra_postargs=None,
build_temp=None,
target_lang=None,
):
# For a built module to use functions from cpython, it needs to use Pythons
# side deck file. The side deck is located beside the libpython3.xx.so
ldversion = sysconfig.get_config_var('LDVERSION')
if sysconfig.python_build:
side_deck_path = os.path.join(
sysconfig.get_config_var('abs_builddir'),
f'libpython{ldversion}.x',
)
else:
side_deck_path = os.path.join(
sysconfig.get_config_var('installed_base'),
sysconfig.get_config_var('platlibdir'),
f'libpython{ldversion}.x',
)
if os.path.exists(side_deck_path):
if extra_postargs:
extra_postargs.append(side_deck_path)
else:
extra_postargs = [side_deck_path]
# Check and replace libraries included side deck files
if runtime_library_dirs:
for dir in runtime_library_dirs:
for library in libraries[:]:
library_side_deck = os.path.join(dir, f'{library}.x')
if os.path.exists(library_side_deck):
libraries.remove(library)
extra_postargs.append(library_side_deck)
break
# Any required ld args for the given compiler
extra_postargs.extend(_ld_args[self.zos_compiler])
super().link(
target_desc,
objects,
output_filename,
output_dir,
libraries,
library_dirs,
runtime_library_dirs,
export_symbols,
debug,
extra_preargs,
extra_postargs,
build_temp,
target_lang,
)