diff --git a/newsfragments/4538.feature.rst b/newsfragments/4538.feature.rst new file mode 100644 index 0000000000..9c36ad5209 --- /dev/null +++ b/newsfragments/4538.feature.rst @@ -0,0 +1 @@ +Merged with distutils@d7ffdb9c7 including: Support for Pathlike objects in data files and extensions (pypa/distutils#272, pypa/distutils#237), native support for C++ compilers (pypa/distuils#228) and removed unused get_msvcr() (pypa/distutils#274). diff --git a/setuptools/_distutils/_collections.py b/setuptools/_distutils/_collections.py index d11a83467c..863030b3cf 100644 --- a/setuptools/_distutils/_collections.py +++ b/setuptools/_distutils/_collections.py @@ -1,11 +1,7 @@ from __future__ import annotations import collections -import functools import itertools -import operator -from collections.abc import Mapping -from typing import Any # from jaraco.collections 3.5.1 @@ -60,144 +56,3 @@ def __contains__(self, other): def __len__(self): return len(list(iter(self))) - - -# from jaraco.collections 5.0.1 -class RangeMap(dict): - """ - A dictionary-like object that uses the keys as bounds for a range. - Inclusion of the value for that range is determined by the - key_match_comparator, which defaults to less-than-or-equal. - A value is returned for a key if it is the first key that matches in - the sorted list of keys. - - One may supply keyword parameters to be passed to the sort function used - to sort keys (i.e. key, reverse) as sort_params. - - Create a map that maps 1-3 -> 'a', 4-6 -> 'b' - - >>> r = RangeMap({3: 'a', 6: 'b'}) # boy, that was easy - >>> r[1], r[2], r[3], r[4], r[5], r[6] - ('a', 'a', 'a', 'b', 'b', 'b') - - Even float values should work so long as the comparison operator - supports it. - - >>> r[4.5] - 'b' - - Notice that the way rangemap is defined, it must be open-ended - on one side. - - >>> r[0] - 'a' - >>> r[-1] - 'a' - - One can close the open-end of the RangeMap by using undefined_value - - >>> r = RangeMap({0: RangeMap.undefined_value, 3: 'a', 6: 'b'}) - >>> r[0] - Traceback (most recent call last): - ... - KeyError: 0 - - One can get the first or last elements in the range by using RangeMap.Item - - >>> last_item = RangeMap.Item(-1) - >>> r[last_item] - 'b' - - .last_item is a shortcut for Item(-1) - - >>> r[RangeMap.last_item] - 'b' - - Sometimes it's useful to find the bounds for a RangeMap - - >>> r.bounds() - (0, 6) - - RangeMap supports .get(key, default) - - >>> r.get(0, 'not found') - 'not found' - - >>> r.get(7, 'not found') - 'not found' - - One often wishes to define the ranges by their left-most values, - which requires use of sort params and a key_match_comparator. - - >>> r = RangeMap({1: 'a', 4: 'b'}, - ... sort_params=dict(reverse=True), - ... key_match_comparator=operator.ge) - >>> r[1], r[2], r[3], r[4], r[5], r[6] - ('a', 'a', 'a', 'b', 'b', 'b') - - That wasn't nearly as easy as before, so an alternate constructor - is provided: - - >>> r = RangeMap.left({1: 'a', 4: 'b', 7: RangeMap.undefined_value}) - >>> r[1], r[2], r[3], r[4], r[5], r[6] - ('a', 'a', 'a', 'b', 'b', 'b') - - """ - - def __init__( - self, - source, - sort_params: Mapping[str, Any] = {}, - key_match_comparator=operator.le, - ): - dict.__init__(self, source) - self.sort_params = sort_params - self.match = key_match_comparator - - @classmethod - def left(cls, source): - return cls( - source, sort_params=dict(reverse=True), key_match_comparator=operator.ge - ) - - def __getitem__(self, item): - sorted_keys = sorted(self.keys(), **self.sort_params) - if isinstance(item, RangeMap.Item): - result = self.__getitem__(sorted_keys[item]) - else: - key = self._find_first_match_(sorted_keys, item) - result = dict.__getitem__(self, key) - if result is RangeMap.undefined_value: - raise KeyError(key) - return result - - def get(self, key, default=None): - """ - Return the value for key if key is in the dictionary, else default. - If default is not given, it defaults to None, so that this method - never raises a KeyError. - """ - try: - return self[key] - except KeyError: - return default - - def _find_first_match_(self, keys, item): - is_match = functools.partial(self.match, item) - matches = list(filter(is_match, keys)) - if matches: - return matches[0] - raise KeyError(item) - - def bounds(self): - sorted_keys = sorted(self.keys(), **self.sort_params) - return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item]) - - # some special values for the RangeMap - undefined_value = type('RangeValueUndefined', (), {})() - - class Item(int): - "RangeMap Item" - - first_item = Item(0) - last_item = Item(-1) diff --git a/setuptools/_distutils/bcppcompiler.py b/setuptools/_distutils/bcppcompiler.py index e47dca5d09..9157b43328 100644 --- a/setuptools/_distutils/bcppcompiler.py +++ b/setuptools/_distutils/bcppcompiler.py @@ -236,8 +236,7 @@ def link( # noqa: C901 temp_dir = os.path.dirname(objects[0]) # preserve tree structure def_file = os.path.join(temp_dir, f'{modname}.def') contents = ['EXPORTS'] - for sym in export_symbols or []: - contents.append(f' {sym}=_{sym}') + contents.extend(f' {sym}=_{sym}' for sym in export_symbols) self.execute(write_file, (def_file, contents), f"writing {def_file}") # Borland C++ has problems with '/' in paths diff --git a/setuptools/_distutils/ccompiler.py b/setuptools/_distutils/ccompiler.py index 9d5297b944..bc4743bcbf 100644 --- a/setuptools/_distutils/ccompiler.py +++ b/setuptools/_distutils/ccompiler.py @@ -22,7 +22,7 @@ ) from .file_util import move_file from .spawn import spawn -from .util import execute, split_quoted, is_mingw +from .util import execute, is_mingw, split_quoted class CCompiler: @@ -1124,10 +1124,10 @@ def show_compilers(): # commands that use it. from distutils.fancy_getopt import FancyGetopt - compilers = [] - for compiler in compiler_class.keys(): - compilers.append(("compiler=" + compiler, None, compiler_class[compiler][2])) - compilers.sort() + compilers = sorted( + ("compiler=" + compiler, None, compiler_class[compiler][2]) + for compiler in compiler_class.keys() + ) pretty_printer = FancyGetopt(compilers) pretty_printer.print_help("List of available compilers:") @@ -1218,8 +1218,7 @@ def gen_preprocess_options(macros, include_dirs): # shell at all costs when we spawn the command! pp_opts.append("-D{}={}".format(*macro)) - for dir in include_dirs: - pp_opts.append(f"-I{dir}") + pp_opts.extend(f"-I{dir}" for dir in include_dirs) return pp_opts @@ -1230,10 +1229,7 @@ def gen_lib_options(compiler, library_dirs, runtime_library_dirs, libraries): directories. Returns a list of command-line options suitable for use with some compiler (depending on the two format strings passed in). """ - lib_opts = [] - - for dir in library_dirs: - lib_opts.append(compiler.library_dir_option(dir)) + lib_opts = [compiler.library_dir_option(dir) for dir in library_dirs] for dir in runtime_library_dirs: lib_opts.extend(always_iterable(compiler.runtime_library_dir_option(dir))) diff --git a/setuptools/_distutils/command/bdist.py b/setuptools/_distutils/command/bdist.py index 1738f4e56b..f334075159 100644 --- a/setuptools/_distutils/command/bdist.py +++ b/setuptools/_distutils/command/bdist.py @@ -15,9 +15,10 @@ def show_formats(): """Print list of available formats (arguments to "--format" option).""" from ..fancy_getopt import FancyGetopt - formats = [] - for format in bdist.format_commands: - formats.append(("formats=" + format, None, bdist.format_commands[format][1])) + formats = [ + ("formats=" + format, None, bdist.format_commands[format][1]) + for format in bdist.format_commands + ] pretty_printer = FancyGetopt(formats) pretty_printer.print_help("List of available distribution formats:") diff --git a/setuptools/_distutils/command/build_ext.py b/setuptools/_distutils/command/build_ext.py index 18e1601a28..cf475fe824 100644 --- a/setuptools/_distutils/command/build_ext.py +++ b/setuptools/_distutils/command/build_ext.py @@ -465,10 +465,7 @@ def get_outputs(self): # And build the list of output (built) filenames. Note that this # ignores the 'inplace' flag, and assumes everything goes in the # "build" tree. - outputs = [] - for ext in self.extensions: - outputs.append(self.get_ext_fullpath(ext.name)) - return outputs + return [self.get_ext_fullpath(ext.name) for ext in self.extensions] def build_extensions(self): # First, sanity-check the 'extensions' list diff --git a/setuptools/_distutils/command/check.py b/setuptools/_distutils/command/check.py index 58b3f949f9..93d754e73d 100644 --- a/setuptools/_distutils/command/check.py +++ b/setuptools/_distutils/command/check.py @@ -100,10 +100,9 @@ def check_metadata(self): """ metadata = self.distribution.metadata - missing = [] - for attr in 'name', 'version': - if not getattr(metadata, attr, None): - missing.append(attr) + missing = [ + attr for attr in ('name', 'version') if not getattr(metadata, attr, None) + ] if missing: self.warn("missing required meta-data: {}".format(', '.join(missing))) diff --git a/setuptools/_distutils/command/install_data.py b/setuptools/_distutils/command/install_data.py index 624c0b901b..a90ec3b4d0 100644 --- a/setuptools/_distutils/command/install_data.py +++ b/setuptools/_distutils/command/install_data.py @@ -5,7 +5,11 @@ # contributed by Bastian Kleineidam +from __future__ import annotations + +import functools import os +from typing import Iterable from ..core import Command from ..util import change_root, convert_path @@ -46,36 +50,42 @@ def finalize_options(self): def run(self): self.mkpath(self.install_dir) for f in self.data_files: - if isinstance(f, str): - # it's a simple file, so copy it - f = convert_path(f) - if self.warn_dir: - self.warn( - "setup script did not provide a directory for " - f"'{f}' -- installing right in '{self.install_dir}'" - ) - (out, _) = self.copy_file(f, self.install_dir) + self._copy(f) + + @functools.singledispatchmethod + def _copy(self, f: tuple[str | os.PathLike, Iterable[str | os.PathLike]]): + # it's a tuple with path to install to and a list of files + dir = convert_path(f[0]) + if not os.path.isabs(dir): + dir = os.path.join(self.install_dir, dir) + elif self.root: + dir = change_root(self.root, dir) + self.mkpath(dir) + + if f[1] == []: + # If there are no files listed, the user must be + # trying to create an empty directory, so add the + # directory to the list of output files. + self.outfiles.append(dir) + else: + # Copy files, adding them to the list of output files. + for data in f[1]: + data = convert_path(data) + (out, _) = self.copy_file(data, dir) self.outfiles.append(out) - else: - # it's a tuple with path to install to and a list of files - dir = convert_path(f[0]) - if not os.path.isabs(dir): - dir = os.path.join(self.install_dir, dir) - elif self.root: - dir = change_root(self.root, dir) - self.mkpath(dir) - - if f[1] == []: - # If there are no files listed, the user must be - # trying to create an empty directory, so add the - # directory to the list of output files. - self.outfiles.append(dir) - else: - # Copy files, adding them to the list of output files. - for data in f[1]: - data = convert_path(data) - (out, _) = self.copy_file(data, dir) - self.outfiles.append(out) + + @_copy.register(str) + @_copy.register(os.PathLike) + def _(self, f: str | os.PathLike): + # it's a simple file, so copy it + f = convert_path(f) + if self.warn_dir: + self.warn( + "setup script did not provide a directory for " + f"'{f}' -- installing right in '{self.install_dir}'" + ) + (out, _) = self.copy_file(f, self.install_dir) + self.outfiles.append(out) def get_inputs(self): return self.data_files or [] diff --git a/setuptools/_distutils/command/install_lib.py b/setuptools/_distutils/command/install_lib.py index 54a12d38a8..01579d46b4 100644 --- a/setuptools/_distutils/command/install_lib.py +++ b/setuptools/_distutils/command/install_lib.py @@ -161,9 +161,7 @@ def _mutate_outputs(self, has_any, build_cmd, cmd_option, output_dir): build_dir = getattr(build_cmd, cmd_option) prefix_len = len(build_dir) + len(os.sep) - outputs = [] - for file in build_files: - outputs.append(os.path.join(output_dir, file[prefix_len:])) + outputs = [os.path.join(output_dir, file[prefix_len:]) for file in build_files] return outputs diff --git a/setuptools/_distutils/command/sdist.py b/setuptools/_distutils/command/sdist.py index 04333dd214..e8abb73920 100644 --- a/setuptools/_distutils/command/sdist.py +++ b/setuptools/_distutils/command/sdist.py @@ -24,10 +24,10 @@ def show_formats(): from ..archive_util import ARCHIVE_FORMATS from ..fancy_getopt import FancyGetopt - formats = [] - for format in ARCHIVE_FORMATS.keys(): - formats.append(("formats=" + format, None, ARCHIVE_FORMATS[format][2])) - formats.sort() + formats = sorted( + ("formats=" + format, None, ARCHIVE_FORMATS[format][2]) + for format in ARCHIVE_FORMATS.keys() + ) FancyGetopt(formats).print_help("List of available source distribution formats:") diff --git a/setuptools/_distutils/compat/py38.py b/setuptools/_distutils/compat/py38.py index 2d44211147..03ec73ef0e 100644 --- a/setuptools/_distutils/compat/py38.py +++ b/setuptools/_distutils/compat/py38.py @@ -14,6 +14,7 @@ def removeprefix(self, prefix): return self[len(prefix) :] else: return self[:] + else: def removesuffix(self, suffix): diff --git a/setuptools/_distutils/cygwinccompiler.py b/setuptools/_distutils/cygwinccompiler.py index 7b812fd055..ce412e8329 100644 --- a/setuptools/_distutils/cygwinccompiler.py +++ b/setuptools/_distutils/cygwinccompiler.py @@ -9,13 +9,11 @@ import copy import os import pathlib -import re import shlex import sys import warnings from subprocess import check_output -from ._collections import RangeMap from .errors import ( CCompilerError, CompileError, @@ -26,42 +24,10 @@ from .unixccompiler import UnixCCompiler from .version import LooseVersion, suppress_known_deprecation -_msvcr_lookup = RangeMap.left( - { - # MSVC 7.0 - 1300: ['msvcr70'], - # MSVC 7.1 - 1310: ['msvcr71'], - # VS2005 / MSVC 8.0 - 1400: ['msvcr80'], - # VS2008 / MSVC 9.0 - 1500: ['msvcr90'], - # VS2010 / MSVC 10.0 - 1600: ['msvcr100'], - # VS2012 / MSVC 11.0 - 1700: ['msvcr110'], - # VS2013 / MSVC 12.0 - 1800: ['msvcr120'], - # VS2015 / MSVC 14.0 - 1900: ['vcruntime140'], - 2000: RangeMap.undefined_value, - }, -) - def get_msvcr(): - """Include the appropriate MSVC runtime library if Python was built - with MSVC 7.0 or later. - """ - match = re.search(r'MSC v\.(\d{4})', sys.version) - try: - msc_ver = int(match.group(1)) - except AttributeError: - return [] - try: - return _msvcr_lookup[msc_ver] - except KeyError: - raise ValueError(f"Unknown MS Compiler version {msc_ver} ") + """No longer needed, but kept for backward compatibility.""" + return [] _runtime_library_dirs_msg = ( @@ -99,18 +65,20 @@ def __init__(self, verbose=False, dry_run=False, force=False): self.cxx = os.environ.get('CXX', '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_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}', ) - # Include the appropriate MSVC runtime library if Python was built - # with MSVC 7.0 or later. self.dll_libraries = get_msvcr() @property @@ -138,9 +106,17 @@ def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): raise CompileError(msg) else: # for other files use the C-compiler try: - self.spawn( - self.compiler_so + cc_args + [src, '-o', obj] + extra_postargs - ) + 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) @@ -276,9 +252,12 @@ def __init__(self, verbose=False, dry_run=False, force=False): 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): diff --git a/setuptools/_distutils/dist.py b/setuptools/_distutils/dist.py index d7d4ca8fc8..0a57d60be9 100644 --- a/setuptools/_distutils/dist.py +++ b/setuptools/_distutils/dist.py @@ -745,10 +745,7 @@ def print_commands(self): for cmd in std_commands: is_std.add(cmd) - extra_commands = [] - for cmd in self.cmdclass.keys(): - if cmd not in is_std: - extra_commands.append(cmd) + extra_commands = [cmd for cmd in self.cmdclass.keys() if cmd not in is_std] max_length = 0 for cmd in std_commands + extra_commands: @@ -776,10 +773,7 @@ def get_command_list(self): for cmd in std_commands: is_std.add(cmd) - extra_commands = [] - for cmd in self.cmdclass.keys(): - if cmd not in is_std: - extra_commands.append(cmd) + extra_commands = [cmd for cmd in self.cmdclass.keys() if cmd not in is_std] rv = [] for cmd in std_commands + extra_commands: @@ -1301,7 +1295,4 @@ def fix_help_options(options): """Convert a 4-tuple 'help_options' list as found in various command classes to the 3-tuple form required by FancyGetopt. """ - new_options = [] - for help_tuple in options: - new_options.append(help_tuple[0:3]) - return new_options + return [opt[0:3] for opt in options] diff --git a/setuptools/_distutils/extension.py b/setuptools/_distutils/extension.py index 04e871bcd6..b302082f7a 100644 --- a/setuptools/_distutils/extension.py +++ b/setuptools/_distutils/extension.py @@ -26,7 +26,7 @@ class Extension: name : string the full name of the extension, including any packages -- ie. *not* a filename or pathname, but Python dotted name - sources : [string] + sources : [string | os.PathLike] list of source filenames, relative to the distribution root (where the setup script lives), in Unix form (slash-separated) for portability. Source files may be C, C++, SWIG (.i), @@ -106,11 +106,16 @@ def __init__( ): if not isinstance(name, str): raise AssertionError("'name' must be a string") - if not (isinstance(sources, list) and all(isinstance(v, str) for v in sources)): - raise AssertionError("'sources' must be a list of strings") + if not ( + isinstance(sources, list) + and all(isinstance(v, (str, os.PathLike)) for v in sources) + ): + raise AssertionError( + "'sources' must be a list of strings or PathLike objects." + ) self.name = name - self.sources = sources + self.sources = list(map(os.fspath, sources)) self.include_dirs = include_dirs or [] self.define_macros = define_macros or [] self.undef_macros = undef_macros or [] diff --git a/setuptools/_distutils/msvc9compiler.py b/setuptools/_distutils/msvc9compiler.py index f860a8d383..4c70848730 100644 --- a/setuptools/_distutils/msvc9compiler.py +++ b/setuptools/_distutils/msvc9compiler.py @@ -640,9 +640,7 @@ def link( # noqa: C901 else: ldflags = self.ldflags_shared - export_opts = [] - for sym in export_symbols or []: - export_opts.append("/EXPORT:" + sym) + export_opts = [f"/EXPORT:{sym}" for sym in export_symbols or []] ld_args = ( ldflags + lib_opts + export_opts + objects + ['/OUT:' + output_filename] diff --git a/setuptools/_distutils/msvccompiler.py b/setuptools/_distutils/msvccompiler.py index 2bf94e60c9..2a5e61d78d 100644 --- a/setuptools/_distutils/msvccompiler.py +++ b/setuptools/_distutils/msvccompiler.py @@ -534,9 +534,7 @@ def link( # noqa: C901 else: ldflags = self.ldflags_shared - export_opts = [] - for sym in export_symbols or []: - export_opts.append("/EXPORT:" + sym) + export_opts = [f"/EXPORT:{sym}" for sym in export_symbols or []] ld_args = ( ldflags + lib_opts + export_opts + objects + ['/OUT:' + output_filename] diff --git a/setuptools/_distutils/spawn.py b/setuptools/_distutils/spawn.py index 50d30a2761..107b011397 100644 --- a/setuptools/_distutils/spawn.py +++ b/setuptools/_distutils/spawn.py @@ -12,7 +12,6 @@ import subprocess import sys import warnings - from typing import Mapping from ._log import log diff --git a/setuptools/_distutils/sysconfig.py b/setuptools/_distutils/sysconfig.py index 7ebe67687e..fbdd5d73ae 100644 --- a/setuptools/_distutils/sysconfig.py +++ b/setuptools/_distutils/sysconfig.py @@ -287,7 +287,7 @@ def _customize_macos(): ) -def customize_compiler(compiler): # noqa: C901 +def customize_compiler(compiler): """Do any platform-specific customization of a CCompiler instance. Mainly needed on Unix, so we can plug in the information that @@ -304,6 +304,7 @@ def customize_compiler(compiler): # noqa: C901 cflags, ccshared, ldshared, + ldcxxshared, shlib_suffix, ar, ar_flags, @@ -313,11 +314,14 @@ def customize_compiler(compiler): # noqa: C901 'CFLAGS', 'CCSHARED', 'LDSHARED', + 'LDCXXSHARED', 'SHLIB_SUFFIX', 'AR', 'ARFLAGS', ) + cxxflags = cflags + if 'CC' in os.environ: newcc = os.environ['CC'] if 'LDSHARED' not in os.environ and ldshared.startswith(cc): @@ -325,38 +329,42 @@ def customize_compiler(compiler): # noqa: C901 # command for LDSHARED as well ldshared = newcc + ldshared[len(cc) :] cc = newcc - if 'CXX' in os.environ: - cxx = os.environ['CXX'] - if 'LDSHARED' in os.environ: - ldshared = os.environ['LDSHARED'] - if 'CPP' in os.environ: - cpp = os.environ['CPP'] - else: - cpp = cc + " -E" # not always - if 'LDFLAGS' in os.environ: - ldshared = ldshared + ' ' + os.environ['LDFLAGS'] - if 'CFLAGS' in os.environ: - cflags = cflags + ' ' + os.environ['CFLAGS'] - ldshared = ldshared + ' ' + os.environ['CFLAGS'] - if 'CPPFLAGS' in os.environ: - cpp = cpp + ' ' + os.environ['CPPFLAGS'] - cflags = cflags + ' ' + os.environ['CPPFLAGS'] - ldshared = ldshared + ' ' + os.environ['CPPFLAGS'] - if 'AR' in os.environ: - ar = os.environ['AR'] - if 'ARFLAGS' in os.environ: - archiver = ar + ' ' + os.environ['ARFLAGS'] - else: - archiver = ar + ' ' + ar_flags + cxx = os.environ.get('CXX', cxx) + ldshared = os.environ.get('LDSHARED', ldshared) + ldcxxshared = os.environ.get('LDCXXSHARED', ldcxxshared) + cpp = os.environ.get( + 'CPP', + cc + " -E", # not always + ) + ldshared = _add_flags(ldshared, 'LD') + ldcxxshared = _add_flags(ldcxxshared, 'LD') + cflags = _add_flags(cflags, 'C') + ldshared = _add_flags(ldshared, 'C') + cxxflags = os.environ.get('CXXFLAGS', cxxflags) + ldcxxshared = _add_flags(ldcxxshared, 'CXX') + cpp = _add_flags(cpp, 'CPP') + cflags = _add_flags(cflags, 'CPP') + cxxflags = _add_flags(cxxflags, 'CPP') + ldshared = _add_flags(ldshared, 'CPP') + ldcxxshared = _add_flags(ldcxxshared, 'CPP') + + ar = os.environ.get('AR', ar) + + archiver = ar + ' ' + os.environ.get('ARFLAGS', ar_flags) cc_cmd = cc + ' ' + cflags + cxx_cmd = cxx + ' ' + cxxflags + compiler.set_executables( preprocessor=cpp, compiler=cc_cmd, compiler_so=cc_cmd + ' ' + ccshared, - compiler_cxx=cxx, + compiler_cxx=cxx_cmd, + compiler_so_cxx=cxx_cmd + ' ' + ccshared, linker_so=ldshared, + linker_so_cxx=ldcxxshared, linker_exe=cc, + linker_exe_cxx=cxx, archiver=archiver, ) @@ -561,3 +569,14 @@ def get_config_var(name): warnings.warn('SO is deprecated, use EXT_SUFFIX', DeprecationWarning, 2) return get_config_vars().get(name) + + +@pass_none +def _add_flags(value: str, type: str) -> str: + """ + Add any flags from the environment for the given type. + + type is the prefix to FLAGS in the environment key (e.g. "C" for "CFLAGS"). + """ + flags = os.environ.get(f'{type}FLAGS') + return f'{value} {flags}' if flags else value diff --git a/setuptools/_distutils/tests/test_archive_util.py b/setuptools/_distutils/tests/test_archive_util.py index abbcd36cb0..389eba16e8 100644 --- a/setuptools/_distutils/tests/test_archive_util.py +++ b/setuptools/_distutils/tests/test_archive_util.py @@ -18,10 +18,10 @@ from distutils.spawn import spawn from distutils.tests import support from os.path import splitdrive -from test.support import patch import path import pytest +from test.support import patch from .compat.py38 import check_warnings from .unix_compat import UID_0_SUPPORT, grp, pwd, require_uid_0, require_unix_id diff --git a/setuptools/_distutils/tests/test_build.py b/setuptools/_distutils/tests/test_build.py index 8fb1bc1b77..d379aca0bb 100644 --- a/setuptools/_distutils/tests/test_build.py +++ b/setuptools/_distutils/tests/test_build.py @@ -4,8 +4,7 @@ import sys from distutils.command.build import build from distutils.tests import support -from sysconfig import get_config_var -from sysconfig import get_platform +from sysconfig import get_config_var, get_platform class TestBuild(support.TempdirManager): diff --git a/setuptools/_distutils/tests/test_build_ext.py b/setuptools/_distutils/tests/test_build_ext.py index 6c4c4ba869..8bd3cef855 100644 --- a/setuptools/_distutils/tests/test_build_ext.py +++ b/setuptools/_distutils/tests/test_build_ext.py @@ -25,11 +25,11 @@ fixup_build_ext, ) from io import StringIO -from test import support import jaraco.path import path import pytest +from test import support from .compat import py38 as import_helper diff --git a/setuptools/_distutils/tests/test_cygwinccompiler.py b/setuptools/_distutils/tests/test_cygwinccompiler.py index 2e1640b757..677bc0ac99 100644 --- a/setuptools/_distutils/tests/test_cygwinccompiler.py +++ b/setuptools/_distutils/tests/test_cygwinccompiler.py @@ -71,50 +71,8 @@ def test_check_config_h(self): assert check_config_h()[0] == CONFIG_H_OK def test_get_msvcr(self): - # [] - sys.version = ( - '2.6.1 (r261:67515, Dec 6 2008, 16:42:21) ' - '\n[GCC 4.0.1 (Apple Computer, Inc. build 5370)]' - ) assert get_msvcr() == [] - # MSVC 7.0 - sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1300 32 bits (Intel)]' - ) - assert get_msvcr() == ['msvcr70'] - - # MSVC 7.1 - sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bits (Intel)]' - ) - assert get_msvcr() == ['msvcr71'] - - # VS2005 / MSVC 8.0 - sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1400 32 bits (Intel)]' - ) - assert get_msvcr() == ['msvcr80'] - - # VS2008 / MSVC 9.0 - sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1500 32 bits (Intel)]' - ) - assert get_msvcr() == ['msvcr90'] - - sys.version = ( - '3.10.0 (tags/v3.10.0:b494f59, Oct 4 2021, 18:46:30) ' - '[MSC v.1929 32 bit (Intel)]' - ) - assert get_msvcr() == ['vcruntime140'] - - # unknown - sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.2000 32 bits (Intel)]' - ) - with pytest.raises(ValueError): - get_msvcr() - @pytest.mark.skipif('sys.platform != "cygwin"') def test_dll_libraries_not_none(self): from distutils.cygwinccompiler import CygwinCCompiler diff --git a/setuptools/_distutils/tests/test_dist.py b/setuptools/_distutils/tests/test_dist.py index 5bd206fec1..4d78a19803 100644 --- a/setuptools/_distutils/tests/test_dist.py +++ b/setuptools/_distutils/tests/test_dist.py @@ -88,7 +88,7 @@ def test_command_packages_cmdline(self, clear_argv): 'distutils' not in Distribution.parse_config_files.__module__, reason='Cannot test when virtualenv has monkey-patched Distribution', ) - def test_venv_install_options(self, tmp_path): + def test_venv_install_options(self, tmp_path, clear_argv): sys.argv.append("install") file = str(tmp_path / 'file') diff --git a/setuptools/_distutils/tests/test_extension.py b/setuptools/_distutils/tests/test_extension.py index 527a135506..41872e04e8 100644 --- a/setuptools/_distutils/tests/test_extension.py +++ b/setuptools/_distutils/tests/test_extension.py @@ -1,6 +1,7 @@ """Tests for distutils.extension.""" import os +import pathlib import warnings from distutils.extension import Extension, read_setup_file @@ -68,13 +69,15 @@ def test_extension_init(self): assert ext.name == 'name' # the second argument, which is the list of files, must - # be a list of strings + # be a list of strings or PathLike objects with pytest.raises(AssertionError): Extension('name', 'file') with pytest.raises(AssertionError): Extension('name', ['file', 1]) ext = Extension('name', ['file1', 'file2']) assert ext.sources == ['file1', 'file2'] + ext = Extension('name', [pathlib.Path('file1'), pathlib.Path('file2')]) + assert ext.sources == ['file1', 'file2'] # others arguments have defaults for attr in ( diff --git a/setuptools/_distutils/tests/test_install_data.py b/setuptools/_distutils/tests/test_install_data.py index f34070b10b..c800f86c64 100644 --- a/setuptools/_distutils/tests/test_install_data.py +++ b/setuptools/_distutils/tests/test_install_data.py @@ -1,6 +1,7 @@ """Tests for distutils.command.install_data.""" import os +import pathlib from distutils.command.install_data import install_data from distutils.tests import support @@ -18,22 +19,27 @@ def test_simple_run(self): # data_files can contain # - simple files + # - a Path object # - a tuple with a path, and a list of file one = os.path.join(pkg_dir, 'one') self.write_file(one, 'xxx') inst2 = os.path.join(pkg_dir, 'inst2') two = os.path.join(pkg_dir, 'two') self.write_file(two, 'xxx') + three = pathlib.Path(pkg_dir) / 'three' + self.write_file(three, 'xxx') - cmd.data_files = [one, (inst2, [two])] - assert cmd.get_inputs() == [one, (inst2, [two])] + cmd.data_files = [one, (inst2, [two]), three] + assert cmd.get_inputs() == [one, (inst2, [two]), three] # let's run the command cmd.ensure_finalized() cmd.run() # let's check the result - assert len(cmd.get_outputs()) == 2 + assert len(cmd.get_outputs()) == 3 + rthree = os.path.split(one)[-1] + assert os.path.exists(os.path.join(inst, rthree)) rtwo = os.path.split(two)[-1] assert os.path.exists(os.path.join(inst2, rtwo)) rone = os.path.split(one)[-1] @@ -46,21 +52,23 @@ def test_simple_run(self): cmd.run() # let's check the result - assert len(cmd.get_outputs()) == 2 + assert len(cmd.get_outputs()) == 3 + assert os.path.exists(os.path.join(inst, rthree)) assert os.path.exists(os.path.join(inst2, rtwo)) assert os.path.exists(os.path.join(inst, rone)) cmd.outfiles = [] # now using root and empty dir cmd.root = os.path.join(pkg_dir, 'root') - inst4 = os.path.join(pkg_dir, 'inst4') - three = os.path.join(cmd.install_dir, 'three') - self.write_file(three, 'xx') - cmd.data_files = [one, (inst2, [two]), ('inst3', [three]), (inst4, [])] + inst5 = os.path.join(pkg_dir, 'inst5') + four = os.path.join(cmd.install_dir, 'four') + self.write_file(four, 'xx') + cmd.data_files = [one, (inst2, [two]), three, ('inst5', [four]), (inst5, [])] cmd.ensure_finalized() cmd.run() # let's check the result - assert len(cmd.get_outputs()) == 4 + assert len(cmd.get_outputs()) == 5 + assert os.path.exists(os.path.join(inst, rthree)) assert os.path.exists(os.path.join(inst2, rtwo)) assert os.path.exists(os.path.join(inst, rone)) diff --git a/setuptools/_distutils/tests/test_mingwccompiler.py b/setuptools/_distutils/tests/test_mingwccompiler.py index fd201cd773..3e3ad5058c 100644 --- a/setuptools/_distutils/tests/test_mingwccompiler.py +++ b/setuptools/_distutils/tests/test_mingwccompiler.py @@ -1,8 +1,8 @@ -import pytest - -from distutils.util import split_quoted, is_mingw -from distutils.errors import DistutilsPlatformError, CCompilerError from distutils import sysconfig +from distutils.errors import CCompilerError, DistutilsPlatformError +from distutils.util import is_mingw, split_quoted + +import pytest class TestMingw32CCompiler: @@ -45,6 +45,7 @@ def test_cygwincc_error(self, monkeypatch): with pytest.raises(CCompilerError): distutils.cygwinccompiler.Mingw32CCompiler() + @pytest.mark.skipif('sys.platform == "cygwin"') def test_customize_compiler_with_msvc_python(self): from distutils.cygwinccompiler import Mingw32CCompiler diff --git a/setuptools/_distutils/tests/test_spawn.py b/setuptools/_distutils/tests/test_spawn.py index 2576bdd53d..fd7b669cbf 100644 --- a/setuptools/_distutils/tests/test_spawn.py +++ b/setuptools/_distutils/tests/test_spawn.py @@ -7,10 +7,10 @@ from distutils.errors import DistutilsExecError from distutils.spawn import find_executable, spawn from distutils.tests import support -from test.support import unix_shell import path import pytest +from test.support import unix_shell from .compat import py38 as os_helper diff --git a/setuptools/_distutils/tests/test_sysconfig.py b/setuptools/_distutils/tests/test_sysconfig.py index 889a398c22..49274a36ae 100644 --- a/setuptools/_distutils/tests/test_sysconfig.py +++ b/setuptools/_distutils/tests/test_sysconfig.py @@ -9,12 +9,12 @@ from distutils import sysconfig from distutils.ccompiler import new_compiler # noqa: F401 from distutils.unixccompiler import UnixCCompiler -from test.support import swap_item import jaraco.envs import path import pytest from jaraco.text import trim +from test.support import swap_item def _gen_makefile(root, contents): @@ -134,7 +134,10 @@ def test_customize_compiler(self): assert comp.exes['compiler_so'] == ( 'env_cc --sc-cflags --env-cflags --env-cppflags --sc-ccshared' ) - assert comp.exes['compiler_cxx'] == 'env_cxx --env-cxx-flags' + assert ( + comp.exes['compiler_cxx'] + == 'env_cxx --env-cxx-flags --sc-cflags --env-cppflags' + ) assert comp.exes['linker_exe'] == 'env_cc' assert comp.exes['linker_so'] == ( 'env_ldshared --env-ldflags --env-cflags --env-cppflags' @@ -162,7 +165,7 @@ def test_customize_compiler(self): assert comp.exes['preprocessor'] == 'sc_cc -E' assert comp.exes['compiler'] == 'sc_cc --sc-cflags' assert comp.exes['compiler_so'] == 'sc_cc --sc-cflags --sc-ccshared' - assert comp.exes['compiler_cxx'] == 'sc_cxx' + assert comp.exes['compiler_cxx'] == 'sc_cxx --sc-cflags' assert comp.exes['linker_exe'] == 'sc_cc' assert comp.exes['linker_so'] == 'sc_ldshared' assert comp.shared_lib_extension == 'sc_shutil_suffix' diff --git a/setuptools/_distutils/tests/test_unixccompiler.py b/setuptools/_distutils/tests/test_unixccompiler.py index d2c88e9116..50b66544a8 100644 --- a/setuptools/_distutils/tests/test_unixccompiler.py +++ b/setuptools/_distutils/tests/test_unixccompiler.py @@ -257,9 +257,13 @@ def test_cc_overrides_ldshared_for_cxx_correctly(self): 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' - return 'gcc-4.2' + elif v == 'CC': + return 'gcc-4.2' + return '' def gcvs(*args, _orig=sysconfig.get_config_vars): if args: @@ -315,3 +319,33 @@ def test_has_function(self): self.cc.output_dir = 'scratch' os.chdir(self.mkdtemp()) self.cc.has_function('abort') + + def test_find_library_file(self, monkeypatch): + compiler = UnixCCompiler() + 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' + ) diff --git a/setuptools/_distutils/tests/test_util.py b/setuptools/_distutils/tests/test_util.py index 0de4e1a59c..00c9743ed0 100644 --- a/setuptools/_distutils/tests/test_util.py +++ b/setuptools/_distutils/tests/test_util.py @@ -5,6 +5,7 @@ import email.policy import io import os +import pathlib import sys import sysconfig as stdlib_sysconfig import unittest.mock as mock @@ -63,30 +64,9 @@ def test_get_platform(self): assert get_platform() == 'win-arm64' def test_convert_path(self): - # linux/mac - os.sep = '/' - - def _join(path): - return '/'.join(path) - - os.path.join = _join - - assert convert_path('/home/to/my/stuff') == '/home/to/my/stuff' - - # win - os.sep = '\\' - - def _join(*path): - return '\\'.join(path) - - os.path.join = _join - - with pytest.raises(ValueError): - convert_path('/home/to/my/stuff') - with pytest.raises(ValueError): - convert_path('home/to/my/stuff/') - - assert convert_path('home/to/my/stuff') == 'home\\to\\my\\stuff' + expected = os.sep.join(('', 'home', 'to', 'my', 'stuff')) + assert convert_path('/home/to/my/stuff') == expected + assert convert_path(pathlib.Path('/home/to/my/stuff')) == expected assert convert_path('.') == os.curdir def test_change_root(self): diff --git a/setuptools/_distutils/unixccompiler.py b/setuptools/_distutils/unixccompiler.py index 7e68596b26..6c1116ae8f 100644 --- a/setuptools/_distutils/unixccompiler.py +++ b/setuptools/_distutils/unixccompiler.py @@ -118,9 +118,12 @@ class UnixCCompiler(CCompiler): 'preprocessor': None, 'compiler': ["cc"], 'compiler_so': ["cc"], - 'compiler_cxx': ["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, } @@ -187,8 +190,14 @@ def preprocess( 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: - self.spawn(compiler_so + cc_args + [src, '-o', obj] + extra_postargs) + 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) @@ -256,7 +265,13 @@ def link( # building an executable or linker_so (with shared options) # when building a shared library. building_exe = target_desc == CCompiler.EXECUTABLE - linker = (self.linker_exe if building_exe else self.linker_so)[:] + linker = ( + self.linker_exe + if building_exe + else ( + self.linker_so_cxx if target_lang == "c++" else self.linker_so + ) + )[:] if target_lang == "c++" and self.compiler_cxx: env, linker_ne = _split_env(linker) @@ -366,27 +381,11 @@ def _library_root(dir): return os.path.join(match.group(1), dir[1:]) if apply_root else dir def find_library_file(self, dirs, lib, debug=False): - r""" + """ 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. - - >>> compiler = UnixCCompiler() - >>> compiler._library_root = lambda dir: dir - >>> monkeypatch = getfixture('monkeypatch') - >>> monkeypatch.setattr(os.path, 'exists', lambda d: 'existing' in d) - >>> dirs = ('/foo/bar/missing', '/foo/bar/existing') - >>> compiler.find_library_file(dirs, 'abc').replace('\\', '/') - '/foo/bar/existing/libabc.dylib' - >>> compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/') - '/foo/bar/existing/libabc.dylib' - >>> monkeypatch.setattr(os.path, 'exists', - ... lambda d: 'existing' in d and '.a' in d) - >>> compiler.find_library_file(dirs, 'abc').replace('\\', '/') - '/foo/bar/existing/libabc.a' - >>> compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/') - '/foo/bar/existing/libabc.a' """ lib_names = ( self.library_filename(lib, lib_type=type) diff --git a/setuptools/_distutils/util.py b/setuptools/_distutils/util.py index 9db89b0979..4cc6bd283c 100644 --- a/setuptools/_distutils/util.py +++ b/setuptools/_distutils/util.py @@ -4,9 +4,12 @@ one of the other *util.py modules. """ +from __future__ import annotations + import functools import importlib.util import os +import pathlib import re import string import subprocess @@ -14,6 +17,7 @@ import sysconfig import tempfile +from ._functools import pass_none from ._log import log from ._modified import newer from .errors import DistutilsByteCompileError, DistutilsPlatformError @@ -116,33 +120,23 @@ def split_version(s): return [int(n) for n in s.split('.')] -def convert_path(pathname): - """Return 'pathname' as a name that will work on the native filesystem, - i.e. split it on '/' and put it back together again using the current - directory separator. Needed because filenames in the setup script are - always supplied in Unix style, and have to be converted to the local - convention before we can actually use them in the filesystem. Raises - ValueError on non-Unix-ish systems if 'pathname' either starts or - ends with a slash. +@pass_none +def convert_path(pathname: str | os.PathLike) -> str: + r""" + Allow for pathlib.Path inputs, coax to a native path string. + + If None is passed, will just pass it through as + Setuptools relies on this behavior. + + >>> convert_path(None) is None + True + + Removes empty paths. + + >>> convert_path('foo/./bar').replace('\\', '/') + 'foo/bar' """ - if os.sep == '/': - return pathname - if not pathname: - return pathname - if pathname[0] == '/': - raise ValueError(f"path '{pathname}' cannot be absolute") - if pathname[-1] == '/': - raise ValueError(f"path '{pathname}' cannot end with '/'") - - paths = pathname.split('/') - while '.' in paths: - paths.remove('.') - if not paths: - return os.curdir - return os.path.join(*paths) - - -# convert_path () + return os.fspath(pathlib.PurePath(pathname)) def change_root(new_root, pathname):