From 7216ff217be0c0bf1763c7684393f78145a2d893 Mon Sep 17 00:00:00 2001 From: Danny McClanahan <1305167+cosmicexplorer@users.noreply.github.com> Date: Tue, 24 Jul 2018 15:59:40 -0700 Subject: [PATCH] Associate cli arguments with executables and refactor llvm/gcc c/c++ toolchain selection (#6217) ### Problem #5951 explains the problem addressed by moving CLI arguments to individual `Executable` objects -- this reduces greatly the difficulty in generating appropriate command lines for the executables invoked. In this PR, it can be seen to remove a significant amount of repeated boilerplate. Additionally, we weren't distinguishing between a `Linker` to link the compiled object files of `gcc` or `g++` vs `clang` or `clang++`. We were attempting to generate a linker object which would work with *any of* `gcc`, `g++`, `clang`, or `clang++`, and this wasn't really feasible. Along with the above, this made it extremely difficult and error-prone to generate correct command lines / environments for executing the linker, which led to e.g. not being able to find `crti.o` (as one symptom addressed by this problem). ### Solution - Introduce `CToolchain` and `CppToolchain` in `environment.py`, which can be generated from `LLVMCToolchain`, `LLVMCppToolchain`, `GCCCToolchain`, or `GCCCppToolchain`. These toolchain datatypes are created in `native_toolchain.py`, where a single `@rule` for each ensures that no C or C++ compiler that someone can request was made without an accompanying linker, which will be configured to work with the compiler. - Introduce the `extra_args` property to the `Executable` mixin in `environment.py`, which `Executable` subclasses can just declare a datatype field named `extra_args` in order to override. This is used in `native_toolchain.py` to ensure platform-specific arguments and environment variables are set in the same `@rule` which produces a paired compiler and linker -- there is a single place to look at to see where all the process invocation environment variables and command-line arguments are set for a given toolchain. - Introduce the `ArchiveFileMapper` subsystem and use it to declare sets of directories to resolve within our BinaryTool archives `GCC` and `LLVM`. This subsystem allows globbing (and checks that there is a unique expansion), which makes it robust to e.g. platform-specific paths to things like include or lib directories. ### Result Removes several FIXMEs, including heavily-commented parts of `test_native_toolchain.py`. Partially addresses #5951 -- `setup_py.py` still generates its own execution environment from scratch, and this could be made more hygienic in the future. As noted in #6179 and #6205, this PR seems to immediately fix the CI failures in those PRs. --- .../backend/native/config/environment.py | 104 +++--- .../native/subsystems/binaries/binutils.py | 4 +- .../backend/native/subsystems/binaries/gcc.py | 105 ++++-- .../native/subsystems/binaries/llvm.py | 63 ++-- .../native/subsystems/native_toolchain.py | 340 ++++++++++-------- .../backend/native/subsystems/utils/BUILD | 1 + .../subsystems/utils/archive_file_mapper.py | 66 ++++ .../subsystems/utils/parse_search_dirs.py | 46 ++- .../native/subsystems/xcode_cli_tools.py | 17 +- .../pants/backend/native/tasks/c_compile.py | 15 +- .../pants/backend/native/tasks/cpp_compile.py | 26 +- .../native/tasks/link_shared_libraries.py | 35 +- .../backend/native/tasks/native_compile.py | 32 +- src/python/pants/backend/python/register.py | 6 +- .../python/subsystems/python_native_code.py | 134 ++++++- .../tasks/build_local_python_distributions.py | 60 +++- .../pants/backend/python/tasks/setup_py.py | 134 +------ .../native/subsystems/test_libc_resolution.py | 4 +- .../subsystems/test_native_toolchain.py | 212 ++++------- .../test_build_local_python_distributions.py | 7 +- 20 files changed, 782 insertions(+), 629 deletions(-) create mode 100644 src/python/pants/backend/native/subsystems/utils/archive_file_mapper.py diff --git a/src/python/pants/backend/native/config/environment.py b/src/python/pants/backend/native/config/environment.py index ceffe8493db..a72c684cd68 100644 --- a/src/python/pants/backend/native/config/environment.py +++ b/src/python/pants/backend/native/config/environment.py @@ -11,7 +11,7 @@ from pants.engine.rules import SingletonRule from pants.util.objects import datatype from pants.util.osutil import all_normalized_os_names, get_normalized_os_name -from pants.util.strutil import create_path_env_var, safe_shlex_join +from pants.util.strutil import create_path_env_var class Platform(datatype(['normalized_os_name'])): @@ -53,14 +53,24 @@ def path_entries(self): @abstractproperty def library_dirs(self): - """Directories containing shared libraries required for a subprocess to run.""" + """Directories containing shared libraries that must be on the runtime library search path. + + Note: this is for libraries needed for the current Executable to run -- see LinkerMixin below + for libraries that are needed at link time.""" @abstractproperty def exe_filename(self): """The "entry point" -- which file to invoke when PATH is set to `path_entries()`.""" - def get_invocation_environment_dict(self, platform): - lib_env_var = platform.resolve_platform_specific({ + @property + def extra_args(self): + return [] + + _platform = Platform.create() + + @property + def as_invocation_environment_dict(self): + lib_env_var = self._platform.resolve_platform_specific({ 'darwin': lambda: 'DYLD_LIBRARY_PATH', 'linux': lambda: 'LD_LIBRARY_PATH', }) @@ -78,57 +88,46 @@ class Assembler(datatype([ pass -class Linker(datatype([ - 'path_entries', - 'exe_filename', - 'library_dirs', -]), Executable): +class LinkerMixin(Executable): + + @abstractproperty + def linking_library_dirs(self): + """Directories to search for libraries needed at link time.""" + + @property + def as_invocation_environment_dict(self): + ret = super(LinkerMixin, self).as_invocation_environment_dict.copy() - # FIXME(#5951): We need a way to compose executables more hygienically. This could be done - # declaratively -- something like: { 'LIBRARY_PATH': DelimitedPathDirectoryEnvVar(...) }. We - # could also just use safe_shlex_join() and create_path_env_var() and keep all the state in the - # environment -- but then we have to remember to use those each time we specialize. - def get_invocation_environment_dict(self, platform): - ret = super(Linker, self).get_invocation_environment_dict(platform).copy() - - # TODO: set all LDFLAGS in here or in further specializations of Linker instead of in individual - # tasks. - all_ldflags_for_platform = platform.resolve_platform_specific({ - 'darwin': lambda: ['-mmacosx-version-min=10.11'], - 'linux': lambda: [], - }) ret.update({ 'LDSHARED': self.exe_filename, - # FIXME: this overloads the meaning of 'library_dirs' to also mean "directories containing - # static libraries required for creating an executable" (currently, libc). These concepts - # should be distinct. - 'LIBRARY_PATH': create_path_env_var(self.library_dirs), - 'LDFLAGS': safe_shlex_join(all_ldflags_for_platform), + 'LIBRARY_PATH': create_path_env_var(self.linking_library_dirs), }) return ret +class Linker(datatype([ + 'path_entries', + 'exe_filename', + 'library_dirs', + 'linking_library_dirs', + 'extra_args', +]), LinkerMixin): pass + + class CompilerMixin(Executable): @abstractproperty def include_dirs(self): """Directories to search for header files to #include during compilation.""" - # FIXME: LIBRARY_PATH and (DY)?LD_LIBRARY_PATH are used for entirely different purposes, but are - # both sourced from the same `self.library_dirs`! - def get_invocation_environment_dict(self, platform): - ret = super(CompilerMixin, self).get_invocation_environment_dict(platform).copy() + @property + def as_invocation_environment_dict(self): + ret = super(CompilerMixin, self).as_invocation_environment_dict.copy() if self.include_dirs: ret['CPATH'] = create_path_env_var(self.include_dirs) - all_cflags_for_platform = platform.resolve_platform_specific({ - 'darwin': lambda: ['-mmacosx-version-min=10.11'], - 'linux': lambda: [], - }) - ret['CFLAGS'] = safe_shlex_join(all_cflags_for_platform) - return ret @@ -137,10 +136,12 @@ class CCompiler(datatype([ 'exe_filename', 'library_dirs', 'include_dirs', + 'extra_args', ]), CompilerMixin): - def get_invocation_environment_dict(self, platform): - ret = super(CCompiler, self).get_invocation_environment_dict(platform).copy() + @property + def as_invocation_environment_dict(self): + ret = super(CCompiler, self).as_invocation_environment_dict.copy() ret['CC'] = self.exe_filename @@ -152,27 +153,38 @@ class CppCompiler(datatype([ 'exe_filename', 'library_dirs', 'include_dirs', + 'extra_args', ]), CompilerMixin): - def get_invocation_environment_dict(self, platform): - ret = super(CppCompiler, self).get_invocation_environment_dict(platform).copy() + @property + def as_invocation_environment_dict(self): + ret = super(CppCompiler, self).as_invocation_environment_dict.copy() ret['CXX'] = self.exe_filename return ret -# TODO(#4020): These classes are performing the work of variants. -class GCCCCompiler(datatype([('c_compiler', CCompiler)])): pass +# NB: These wrapper classes for LLVM and GCC toolchains are performing the work of variants. A +# CToolchain cannot be requested directly, but native_toolchain.py provides an LLVMCToolchain, +# which contains a CToolchain representing the clang compiler and a linker paired to work with +# objects compiled by that compiler. +class CToolchain(datatype([('c_compiler', CCompiler), ('c_linker', Linker)])): pass + + +class LLVMCToolchain(datatype([('c_toolchain', CToolchain)])): pass + + +class GCCCToolchain(datatype([('c_toolchain', CToolchain)])): pass -class LLVMCCompiler(datatype([('c_compiler', CCompiler)])): pass +class CppToolchain(datatype([('cpp_compiler', CppCompiler), ('cpp_linker', Linker)])): pass -class GCCCppCompiler(datatype([('cpp_compiler', CppCompiler)])): pass +class LLVMCppToolchain(datatype([('cpp_toolchain', CppToolchain)])): pass -class LLVMCppCompiler(datatype([('cpp_compiler', CppCompiler)])): pass +class GCCCppToolchain(datatype([('cpp_toolchain', CppToolchain)])): pass # FIXME: make this an @rule, after we can automatically produce LibcDev and other subsystems in the diff --git a/src/python/pants/backend/native/subsystems/binaries/binutils.py b/src/python/pants/backend/native/subsystems/binaries/binutils.py index a8feeb05eea..63c554261b7 100644 --- a/src/python/pants/backend/native/subsystems/binaries/binutils.py +++ b/src/python/pants/backend/native/subsystems/binaries/binutils.py @@ -30,7 +30,9 @@ def linker(self): return Linker( path_entries=self.path_entries(), exe_filename='ld', - library_dirs=[]) + library_dirs=[], + linking_library_dirs=[], + extra_args=[]) @rule(Assembler, [Select(Binutils)]) diff --git a/src/python/pants/backend/native/subsystems/binaries/gcc.py b/src/python/pants/backend/native/subsystems/binaries/gcc.py index 0f8db035e6f..0d2b253f82a 100644 --- a/src/python/pants/backend/native/subsystems/binaries/gcc.py +++ b/src/python/pants/backend/native/subsystems/binaries/gcc.py @@ -6,65 +6,102 @@ import os -from pants.backend.native.config.environment import (CCompiler, CppCompiler, GCCCCompiler, - GCCCppCompiler) -from pants.backend.native.subsystems.utils.parse_search_dirs import ParseSearchDirs +from pants.backend.native.config.environment import CCompiler, CppCompiler, Platform +from pants.backend.native.subsystems.utils.archive_file_mapper import ArchiveFileMapper from pants.binaries.binary_tool import NativeTool from pants.engine.rules import RootRule, rule from pants.engine.selectors import Select from pants.util.memo import memoized_method, memoized_property -from pants.util.strutil import create_path_env_var class GCC(NativeTool): + """Subsystem wrapping an archive providing a GCC distribution. + + This subsystem provides the gcc and g++ compilers. + + NB: The lib and include dirs provided by this distribution are produced by using known relative + paths into the distribution of GCC provided on Pantsbuild S3. If we change how we distribute GCC, + these methods may have to change. They should be stable to version upgrades, however. + """ options_scope = 'gcc' default_version = '7.3.0' archive_type = 'tgz' @classmethod def subsystem_dependencies(cls): - return super(GCC, cls).subsystem_dependencies() + (ParseSearchDirs.scoped(cls),) + return super(GCC, cls).subsystem_dependencies() + (ArchiveFileMapper.scoped(cls),) @memoized_property - def _parse_search_dirs(self): - return ParseSearchDirs.scoped_instance(self) + def _file_mapper(self): + return ArchiveFileMapper.scoped_instance(self) - def _lib_search_dirs(self, compiler_exe, path_entries): - return self._parse_search_dirs.get_compiler_library_dirs( - compiler_exe, - env={'PATH': create_path_env_var(path_entries)}) + def _filemap(self, all_components_list): + return self._file_mapper.map_files(self.select(), all_components_list) - @memoized_method + @memoized_property def path_entries(self): - return [os.path.join(self.select(), 'bin')] + return self._filemap([('bin',)]) + + @memoized_method + def _common_lib_dirs(self, platform): + lib64_tuples = platform.resolve_platform_specific({ + 'darwin': lambda: [], + 'linux': lambda: [('lib64',)], + }) + return self._filemap(lib64_tuples + [ + ('lib',), + ('lib/gcc',), + ('lib/gcc/*', self.version()), + ]) + + @memoized_property + def _common_include_dirs(self): + return self._filemap([ + ('include',), + ('lib/gcc/*', self.version(), 'include'), + ]) - def c_compiler(self): - exe_filename = 'gcc' - path_entries = self.path_entries() + def c_compiler(self, platform): return CCompiler( - path_entries=path_entries, - exe_filename=exe_filename, - library_dirs=self._lib_search_dirs(exe_filename, path_entries), - include_dirs=[]) - - def cpp_compiler(self): - exe_filename = 'g++' - path_entries = self.path_entries() + path_entries=self.path_entries, + exe_filename='gcc', + library_dirs=self._common_lib_dirs(platform), + include_dirs=self._common_include_dirs, + extra_args=[]) + + @memoized_property + def _cpp_include_dirs(self): + most_cpp_include_dirs = self._filemap([ + ('include/c++', self.version()), + ]) + + # This file is needed for C++ compilation. + cpp_config_header_path = self._file_mapper.assert_single_path_by_glob( + # NB: There are multiple paths matching this glob unless we provide the full path to + # c++config.h, which is why we bypass self._filemap() here. + [self.select(), 'include/c++', self.version(), '*/bits/c++config.h']) + # Get the directory that makes `#include ` work. + plat_cpp_header_dir = os.path.dirname(os.path.dirname(cpp_config_header_path)) + + return most_cpp_include_dirs + [plat_cpp_header_dir] + + def cpp_compiler(self, platform): return CppCompiler( - path_entries=self.path_entries(), - exe_filename=exe_filename, - library_dirs=self._lib_search_dirs(exe_filename, path_entries), - include_dirs=[]) + path_entries=self.path_entries, + exe_filename='g++', + library_dirs=self._common_lib_dirs(platform), + include_dirs=(self._common_include_dirs + self._cpp_include_dirs), + extra_args=[]) -@rule(GCCCCompiler, [Select(GCC)]) -def get_gcc(gcc): - yield GCCCCompiler(gcc.c_compiler()) +@rule(CCompiler, [Select(GCC), Select(Platform)]) +def get_gcc(gcc, platform): + return gcc.c_compiler(platform) -@rule(GCCCppCompiler, [Select(GCC)]) -def get_gplusplus(gcc): - yield GCCCppCompiler(gcc.cpp_compiler()) +@rule(CppCompiler, [Select(GCC), Select(Platform)]) +def get_gplusplus(gcc, platform): + return gcc.cpp_compiler(platform) def create_gcc_rules(): diff --git a/src/python/pants/backend/native/subsystems/binaries/llvm.py b/src/python/pants/backend/native/subsystems/binaries/llvm.py index 6a97639d2a3..fe6476d97aa 100644 --- a/src/python/pants/backend/native/subsystems/binaries/llvm.py +++ b/src/python/pants/backend/native/subsystems/binaries/llvm.py @@ -6,8 +6,8 @@ import os -from pants.backend.native.config.environment import (CCompiler, CppCompiler, Linker, LLVMCCompiler, - LLVMCppCompiler, Platform) +from pants.backend.native.config.environment import CCompiler, CppCompiler, Linker, Platform +from pants.backend.native.subsystems.utils.archive_file_mapper import ArchiveFileMapper from pants.binaries.binary_tool import NativeTool from pants.binaries.binary_util import BinaryToolUrlGenerator from pants.engine.rules import RootRule, rule @@ -36,6 +36,16 @@ def generate_urls(self, version, host_platform): class LLVM(NativeTool): + """Subsystem wrapping an archive providing an LLVM distribution. + + This subsystem provides the clang and clang++ compilers. It also provides lld, which is not + currently used. + + NB: The lib and include dirs provided by this distribution are produced by using known relative + paths into the distribution of LLVM from LLVMReleaseUrlGenerator. If LLVM changes the structure of + their release archives, these methods may have to change. They should be stable to version + upgrades, however. + """ options_scope = 'llvm' default_version = '6.0.0' archive_type = 'txz' @@ -47,7 +57,7 @@ def get_external_url_generator(self): def select(self): unpacked_path = super(LLVM, self).select() # The archive from releases.llvm.org wraps the extracted content into a directory one level - # deeper, but the one from our S3 does not. + # deeper, but the one from our S3 does not. We account for both here. children = os.listdir(unpacked_path) if len(children) == 1: llvm_base_dir = os.path.join(unpacked_path, children[0]) @@ -55,8 +65,20 @@ def select(self): return llvm_base_dir return unpacked_path + @classmethod + def subsystem_dependencies(cls): + return super(LLVM, cls).subsystem_dependencies() + (ArchiveFileMapper.scoped(cls),) + + @memoized_property + def _file_mapper(self): + return ArchiveFileMapper.scoped_instance(self) + + def _filemap(self, all_components_list): + return self._file_mapper.map_files(self.select(), all_components_list) + + @memoized_property def path_entries(self): - return [os.path.join(self.select(), 'bin')] + return self._filemap([('bin',)]) _PLATFORM_SPECIFIC_LINKER_NAME = { 'darwin': lambda: 'ld64.lld', @@ -65,39 +87,40 @@ def path_entries(self): def linker(self, platform): return Linker( - path_entries=self.path_entries(), + path_entries=self.path_entries, exe_filename=platform.resolve_platform_specific( self._PLATFORM_SPECIFIC_LINKER_NAME), - library_dirs=[]) + library_dirs=[], + linking_library_dirs=[], + extra_args=[]) - # FIXME: use ParseSearchDirs for this and other include directories -- we shouldn't be trying to - # guess the path here. - # https://github.com/pantsbuild/pants/issues/6143 @memoized_property def _common_include_dirs(self): - return [os.path.join(self.select(), 'lib/clang', self.version(), 'include')] + return self._filemap([('lib/clang', self.version(), 'include')]) @memoized_property def _common_lib_dirs(self): - return [os.path.join(self.select(), 'lib')] + return self._filemap([('lib',)]) def c_compiler(self): return CCompiler( - path_entries=self.path_entries(), + path_entries=self.path_entries, exe_filename='clang', library_dirs=self._common_lib_dirs, - include_dirs=self._common_include_dirs) + include_dirs=self._common_include_dirs, + extra_args=[]) @memoized_property def _cpp_include_dirs(self): - return [os.path.join(self.select(), 'include/c++/v1')] + return self._filemap([('include/c++/v1',)]) def cpp_compiler(self): return CppCompiler( - path_entries=self.path_entries(), + path_entries=self.path_entries, exe_filename='clang++', library_dirs=self._common_lib_dirs, - include_dirs=(self._cpp_include_dirs + self._common_include_dirs)) + include_dirs=(self._cpp_include_dirs + self._common_include_dirs), + extra_args=[]) # FIXME(#5663): use this over the XCode linker! @@ -106,14 +129,14 @@ def get_lld(platform, llvm): return llvm.linker(platform) -@rule(LLVMCCompiler, [Select(LLVM)]) +@rule(CCompiler, [Select(LLVM)]) def get_clang(llvm): - yield LLVMCCompiler(llvm.c_compiler()) + return llvm.c_compiler() -@rule(LLVMCppCompiler, [Select(LLVM)]) +@rule(CppCompiler, [Select(LLVM)]) def get_clang_plusplus(llvm): - yield LLVMCppCompiler(llvm.cpp_compiler()) + return llvm.cpp_compiler() def create_llvm_rules(): diff --git a/src/python/pants/backend/native/subsystems/native_toolchain.py b/src/python/pants/backend/native/subsystems/native_toolchain.py index 34dcf27b30b..c1e6279a5c7 100644 --- a/src/python/pants/backend/native/subsystems/native_toolchain.py +++ b/src/python/pants/backend/native/subsystems/native_toolchain.py @@ -5,8 +5,9 @@ from __future__ import absolute_import, division, print_function, unicode_literals from pants.backend.native.config.environment import (Assembler, CCompiler, CppCompiler, - GCCCCompiler, GCCCppCompiler, Linker, - LLVMCCompiler, LLVMCppCompiler, Platform) + CppToolchain, CToolchain, GCCCppToolchain, + GCCCToolchain, Linker, LLVMCppToolchain, + LLVMCToolchain, Platform) from pants.backend.native.subsystems.binaries.binutils import Binutils from pants.backend.native.subsystems.binaries.gcc import GCC from pants.backend.native.subsystems.binaries.llvm import LLVM @@ -16,6 +17,7 @@ from pants.engine.selectors import Get, Select from pants.subsystem.subsystem import Subsystem from pants.util.memo import memoized_property +from pants.util.objects import datatype class NativeToolchain(Subsystem): @@ -63,211 +65,241 @@ def _libc_dev(self): return LibcDev.scoped_instance(self) -@rule(Linker, [Select(Platform), Select(NativeToolchain)]) -def select_linker(platform, native_toolchain): - # TODO(#5933): make it possible to yield Get with a non-static - # subject type and use `platform.resolve_platform_specific()`, something like: - # linker = platform.resolve_platform_specific({ - # 'darwin': lambda: Get(Linker, XCodeCLITools, native_toolchain._xcode_cli_tools), - # 'linux': lambda: Get(Linker, Binutils, native_toolchain._binutils), - # }) - # - # NB: We need to link through a provided compiler's frontend, and we need to know where all the - # compiler's libraries/etc are, so we set the executable name to the C++ compiler, which can find - # its own set of C++-specific files for the linker if necessary. Using e.g. 'g++' as the linker - # appears to produce byte-identical output when linking even C-only object files, and also - # happens to work when C++ is used. - # Currently, OSX links through the clang++ frontend, and Linux links through the g++ frontend. +@rule(LibcDev, [Select(NativeToolchain)]) +def select_libc_dev(native_toolchain): + yield native_toolchain._libc_dev + + +@rule(Assembler, [Select(Platform), Select(NativeToolchain)]) +def select_assembler(platform, native_toolchain): + if platform.normalized_os_name == 'darwin': + assembler = yield Get(Assembler, XCodeCLITools, native_toolchain._xcode_cli_tools) + else: + assembler = yield Get(Assembler, Binutils, native_toolchain._binutils) + yield assembler + + +class BaseLinker(datatype([('linker', Linker)])): + """A Linker which is not specific to any compiler yet. + + This represents Linker objects provided by subsystems, but may need additional information to be + usable by a specific compiler.""" + + +# TODO: select the appropriate `Platform` in the `@rule` decl using variants! +@rule(BaseLinker, [Select(Platform), Select(NativeToolchain)]) +def select_base_linker(platform, native_toolchain): if platform.normalized_os_name == 'darwin': # TODO(#5663): turn this into LLVM when lld works. linker = yield Get(Linker, XCodeCLITools, native_toolchain._xcode_cli_tools) - llvm_c_compiler = yield Get(LLVMCCompiler, NativeToolchain, native_toolchain) - c_compiler = llvm_c_compiler.c_compiler - llvm_cpp_compiler = yield Get(LLVMCppCompiler, NativeToolchain, native_toolchain) - cpp_compiler = llvm_cpp_compiler.cpp_compiler else: linker = yield Get(Linker, Binutils, native_toolchain._binutils) - gcc_c_compiler = yield Get(GCCCCompiler, NativeToolchain, native_toolchain) - c_compiler = gcc_c_compiler.c_compiler - gcc_cpp_compiler = yield Get(GCCCppCompiler, NativeToolchain, native_toolchain) - cpp_compiler = gcc_cpp_compiler.cpp_compiler - - libc_dirs = native_toolchain._libc_dev.get_libc_dirs(platform) - - # NB: If needing to create an environment for process invocation that could use either a compiler - # or a linker (e.g. when we compile native code from `python_dist()`s), use the environment from - # the linker object (in addition to any further customizations), which has the paths from the C - # and C++ compilers baked in. - # FIXME(#5951): we need a way to compose executables more hygienically. - linker = Linker( - path_entries=( - cpp_compiler.path_entries + - c_compiler.path_entries + - linker.path_entries), - exe_filename=cpp_compiler.exe_filename, - library_dirs=( - libc_dirs + - cpp_compiler.library_dirs + - c_compiler.library_dirs + - linker.library_dirs)) - - yield linker - - -@rule(LLVMCCompiler, [Select(Platform), Select(NativeToolchain)]) -def select_llvm_c_compiler(platform, native_toolchain): - original_llvm_c_compiler = yield Get(LLVMCCompiler, LLVM, native_toolchain._llvm) - provided_clang = original_llvm_c_compiler.c_compiler + base_linker = BaseLinker(linker=linker) + yield base_linker + + +class GCCInstallLocationForLLVM(datatype(['toolchain_dir'])): + """This class is convertible into a list of command line arguments for clang and clang++. + + This is only used on Linux. The option --gcc-toolchain stops clang from searching for another gcc + on the host system. The option appears to only exist on Linux clang and clang++.""" + + @property + def as_clang_argv(self): + return ['--gcc-toolchain={}'.format(self.toolchain_dir)] + + +@rule(GCCInstallLocationForLLVM, [Select(GCC)]) +def select_gcc_install_location(gcc): + return GCCInstallLocationForLLVM(gcc.select()) + + +@rule(LLVMCToolchain, [Select(Platform), Select(NativeToolchain)]) +def select_llvm_c_toolchain(platform, native_toolchain): + provided_clang = yield Get(CCompiler, LLVM, native_toolchain._llvm) + + # These arguments are shared across platforms. + llvm_c_compiler_args = [ + '-x', 'c', '-std=c11', + '-nobuiltininc', + ] if platform.normalized_os_name == 'darwin': xcode_clang = yield Get(CCompiler, XCodeCLITools, native_toolchain._xcode_cli_tools) - clang_with_xcode_paths = CCompiler( + working_c_compiler = CCompiler( path_entries=(provided_clang.path_entries + xcode_clang.path_entries), exe_filename=provided_clang.exe_filename, library_dirs=(provided_clang.library_dirs + xcode_clang.library_dirs), - include_dirs=(xcode_clang.include_dirs + provided_clang.include_dirs)) - final_llvm_c_compiler = LLVMCCompiler(clang_with_xcode_paths) + include_dirs=(provided_clang.include_dirs + xcode_clang.include_dirs), + extra_args=(llvm_c_compiler_args + xcode_clang.extra_args)) else: - gcc_c_compiler = yield Get(GCCCCompiler, GCC, native_toolchain._gcc) - provided_gcc = gcc_c_compiler.c_compiler - clang_with_gcc_libs = CCompiler( + gcc_install = yield Get(GCCInstallLocationForLLVM, GCC, native_toolchain._gcc) + provided_gcc = yield Get(CCompiler, GCC, native_toolchain._gcc) + working_c_compiler = CCompiler( path_entries=provided_clang.path_entries, exe_filename=provided_clang.exe_filename, - # We need this version of GLIBCXX to be able to run, unfortunately. + # We need g++'s version of the GLIBCXX library to be able to run, unfortunately. library_dirs=(provided_gcc.library_dirs + provided_clang.library_dirs), - include_dirs=(provided_clang.include_dirs + provided_gcc.include_dirs)) - final_llvm_c_compiler = LLVMCCompiler(clang_with_gcc_libs) - - yield final_llvm_c_compiler - - -@rule(LLVMCppCompiler, [Select(Platform), Select(NativeToolchain)]) -def select_llvm_cpp_compiler(platform, native_toolchain): - original_llvm_cpp_compiler = yield Get(LLVMCppCompiler, LLVM, native_toolchain._llvm) - provided_clangpp = original_llvm_cpp_compiler.cpp_compiler + include_dirs=provided_gcc.include_dirs, + extra_args=(llvm_c_compiler_args + gcc_install.as_clang_argv)) + + base_linker_wrapper = yield Get(BaseLinker, NativeToolchain, native_toolchain) + base_linker = base_linker_wrapper.linker + libc_dev = yield Get(LibcDev, NativeToolchain, native_toolchain) + working_linker = Linker( + path_entries=(base_linker.path_entries + working_c_compiler.path_entries), + exe_filename=working_c_compiler.exe_filename, + library_dirs=(base_linker.library_dirs + working_c_compiler.library_dirs), + linking_library_dirs=(base_linker.linking_library_dirs + libc_dev.get_libc_dirs(platform)), + extra_args=base_linker.extra_args) + + yield LLVMCToolchain(CToolchain(working_c_compiler, working_linker)) + + +@rule(LLVMCppToolchain, [Select(Platform), Select(NativeToolchain)]) +def select_llvm_cpp_toolchain(platform, native_toolchain): + provided_clangpp = yield Get(CppCompiler, LLVM, native_toolchain._llvm) + + # These arguments are shared across platforms. + llvm_cpp_compiler_args = [ + '-x', 'c++', '-std=c++11', + # This mean we don't use any of the headers from our LLVM distribution's C++ stdlib + # implementation, or any from the host system. Instead, we use include dirs from the + # XCodeCLITools or GCC. + '-nobuiltininc', + '-nostdinc++', + ] if platform.normalized_os_name == 'darwin': xcode_clang = yield Get(CppCompiler, XCodeCLITools, native_toolchain._xcode_cli_tools) - clang_with_xcode_paths = CppCompiler( + working_cpp_compiler = CppCompiler( path_entries=(provided_clangpp.path_entries + xcode_clang.path_entries), exe_filename=provided_clangpp.exe_filename, library_dirs=(provided_clangpp.library_dirs + xcode_clang.library_dirs), - include_dirs=(provided_clangpp.include_dirs + xcode_clang.include_dirs)) - final_llvm_cpp_compiler = LLVMCppCompiler(clang_with_xcode_paths) + include_dirs=(provided_clangpp.include_dirs + xcode_clang.include_dirs), + # On OSX, this uses the libc++ (LLVM) C++ standard library implementation. This is + # feature-complete for OSX, but not for Linux (see https://libcxx.llvm.org/ for more info). + extra_args=(llvm_cpp_compiler_args + xcode_clang.extra_args)) + linking_library_dirs = [] + linker_extra_args = [] else: - gcc_cpp_compiler = yield Get(GCCCppCompiler, GCC, native_toolchain._gcc) - provided_gpp = gcc_cpp_compiler.cpp_compiler - clang_with_gpp_libs = CppCompiler( + gcc_install = yield Get(GCCInstallLocationForLLVM, GCC, native_toolchain._gcc) + provided_gpp = yield Get(CppCompiler, GCC, native_toolchain._gcc) + working_cpp_compiler = CppCompiler( path_entries=provided_clangpp.path_entries, exe_filename=provided_clangpp.exe_filename, - # We need this version of GLIBCXX to be able to run, unfortunately. + # We need g++'s version of the GLIBCXX library to be able to run, unfortunately. library_dirs=(provided_gpp.library_dirs + provided_clangpp.library_dirs), - include_dirs=(provided_clangpp.include_dirs + provided_gpp.include_dirs)) - final_llvm_cpp_compiler = LLVMCppCompiler(clang_with_gpp_libs) - - yield final_llvm_cpp_compiler - - -@rule(GCCCCompiler, [Select(Platform), Select(NativeToolchain)]) -def select_gcc_c_compiler(platform, native_toolchain): - original_gcc_c_compiler = yield Get(GCCCCompiler, GCC, native_toolchain._gcc) - provided_gcc = original_gcc_c_compiler.c_compiler + # NB: we use g++'s headers on Linux, and therefore their C++ standard library. + include_dirs=provided_gpp.include_dirs, + extra_args=(llvm_cpp_compiler_args + gcc_install.as_clang_argv)) + linking_library_dirs = provided_gpp.library_dirs + provided_clangpp.library_dirs + # Ensure we use libstdc++, provided by g++, during the linking stage. + linker_extra_args=['-stdlib=libstdc++'] + + libc_dev = yield Get(LibcDev, NativeToolchain, native_toolchain) + base_linker_wrapper = yield Get(BaseLinker, NativeToolchain, native_toolchain) + base_linker = base_linker_wrapper.linker + working_linker = Linker( + path_entries=(base_linker.path_entries + working_cpp_compiler.path_entries), + exe_filename=working_cpp_compiler.exe_filename, + library_dirs=(base_linker.library_dirs + working_cpp_compiler.library_dirs), + linking_library_dirs=(base_linker.linking_library_dirs + + linking_library_dirs + + libc_dev.get_libc_dirs(platform)), + extra_args=(base_linker.extra_args + linker_extra_args)) + + yield LLVMCppToolchain(CppToolchain(working_cpp_compiler, working_linker)) + + +@rule(GCCCToolchain, [Select(Platform), Select(NativeToolchain)]) +def select_gcc_c_toolchain(platform, native_toolchain): + provided_gcc = yield Get(CCompiler, GCC, native_toolchain._gcc) # GCC needs an assembler, so we provide that (platform-specific) tool here. - if platform.normalized_os_name == 'darwin': - xcode_tools_assembler = yield Get(Assembler, XCodeCLITools, native_toolchain._xcode_cli_tools) - assembler_paths = xcode_tools_assembler.path_entries + assembler = yield Get(Assembler, NativeToolchain, native_toolchain) + if platform.normalized_os_name == 'darwin': # GCC needs access to some headers that are only provided by the XCode toolchain # currently (e.g. "_stdio.h"). These headers are unlikely to change across versions, so this is # probably safe. # TODO: we should be providing all of these (so we can eventually phase out XCodeCLITools # entirely). - # This mutual recursion with select_llvm_c_compiler() works because we only pull in gcc in that - # method if we are on Linux. xcode_clang = yield Get(CCompiler, XCodeCLITools, native_toolchain._xcode_cli_tools) - - new_library_dirs = provided_gcc.library_dirs + xcode_clang.library_dirs new_include_dirs = xcode_clang.include_dirs + provided_gcc.include_dirs else: - binutils_assembler = yield Get(Assembler, Binutils, native_toolchain._binutils) - assembler_paths = binutils_assembler.path_entries - - new_library_dirs = provided_gcc.library_dirs new_include_dirs = provided_gcc.include_dirs - gcc_with_assembler = CCompiler( - path_entries=(provided_gcc.path_entries + assembler_paths), + working_c_compiler = CCompiler( + path_entries=(provided_gcc.path_entries + assembler.path_entries), exe_filename=provided_gcc.exe_filename, - library_dirs=new_library_dirs, - include_dirs=new_include_dirs) + library_dirs=provided_gcc.library_dirs, + include_dirs=new_include_dirs, + extra_args=['-x', 'c', '-std=c11']) - final_gcc_c_compiler = GCCCCompiler(gcc_with_assembler) - yield final_gcc_c_compiler + base_linker_wrapper = yield Get(BaseLinker, NativeToolchain, native_toolchain) + base_linker = base_linker_wrapper.linker + libc_dev = yield Get(LibcDev, NativeToolchain, native_toolchain) + working_linker = Linker( + path_entries=(working_c_compiler.path_entries + base_linker.path_entries), + exe_filename=working_c_compiler.exe_filename, + library_dirs=(base_linker.library_dirs + working_c_compiler.library_dirs), + linking_library_dirs=(base_linker.linking_library_dirs + libc_dev.get_libc_dirs(platform)), + extra_args=base_linker.extra_args) + yield GCCCToolchain(CToolchain(working_c_compiler, working_linker)) -@rule(GCCCppCompiler, [Select(Platform), Select(NativeToolchain)]) -def select_gcc_cpp_compiler(platform, native_toolchain): - original_gcc_cpp_compiler = yield Get(GCCCppCompiler, GCC, native_toolchain._gcc) - provided_gpp = original_gcc_cpp_compiler.cpp_compiler - if platform.normalized_os_name == 'darwin': - xcode_tools_assembler = yield Get(Assembler, XCodeCLITools, native_toolchain._xcode_cli_tools) - assembler_paths = xcode_tools_assembler.path_entries +@rule(GCCCppToolchain, [Select(Platform), Select(NativeToolchain)]) +def select_gcc_cpp_toolchain(platform, native_toolchain): + provided_gpp = yield Get(CppCompiler, GCC, native_toolchain._gcc) - xcode_clangpp = yield Get(CppCompiler, XCodeCLITools, native_toolchain._xcode_cli_tools) + # GCC needs an assembler, so we provide that (platform-specific) tool here. + assembler = yield Get(Assembler, NativeToolchain, native_toolchain) - new_library_dirs = provided_gpp.library_dirs + xcode_clangpp.library_dirs + if platform.normalized_os_name == 'darwin': + # GCC needs access to some headers that are only provided by the XCode toolchain + # currently (e.g. "_stdio.h"). These headers are unlikely to change across versions, so this is + # probably safe. + # TODO: we should be providing all of these (so we can eventually phase out XCodeCLITools + # entirely). + xcode_clangpp = yield Get(CppCompiler, XCodeCLITools, native_toolchain._xcode_cli_tools) new_include_dirs = xcode_clangpp.include_dirs + provided_gpp.include_dirs else: - binutils_assembler = yield Get(Assembler, Binutils, native_toolchain._binutils) - assembler_paths = binutils_assembler.path_entries - - new_library_dirs = provided_gpp.library_dirs new_include_dirs = provided_gpp.include_dirs - gcc_with_assembler = CppCompiler( - path_entries=(provided_gpp.path_entries + assembler_paths), + working_cpp_compiler = CppCompiler( + path_entries=(provided_gpp.path_entries + assembler.path_entries), exe_filename=provided_gpp.exe_filename, - library_dirs=new_library_dirs, - include_dirs=new_include_dirs) - - final_gcc_cpp_compiler = GCCCppCompiler(gcc_with_assembler) - yield final_gcc_cpp_compiler - - -@rule(CCompiler, [Select(NativeToolchain), Select(Platform)]) -def select_c_compiler(native_toolchain, platform): - if platform.normalized_os_name == 'darwin': - llvm_c_compiler = yield Get(LLVMCCompiler, NativeToolchain, native_toolchain) - c_compiler = llvm_c_compiler.c_compiler - else: - gcc_c_compiler = yield Get(GCCCCompiler, NativeToolchain, native_toolchain) - c_compiler = gcc_c_compiler.c_compiler - - yield c_compiler - - -@rule(CppCompiler, [Select(NativeToolchain), Select(Platform)]) -def select_cpp_compiler(native_toolchain, platform): - if platform.normalized_os_name == 'darwin': - llvm_cpp_compiler = yield Get(LLVMCppCompiler, NativeToolchain, native_toolchain) - cpp_compiler = llvm_cpp_compiler.cpp_compiler - else: - gcc_cpp_compiler = yield Get(GCCCppCompiler, NativeToolchain, native_toolchain) - cpp_compiler = gcc_cpp_compiler.cpp_compiler - - yield cpp_compiler + library_dirs=provided_gpp.library_dirs, + include_dirs=new_include_dirs, + extra_args=([ + '-x', 'c++', '-std=c++11', + '-nostdinc++', + ])) + + base_linker_wrapper = yield Get(BaseLinker, NativeToolchain, native_toolchain) + base_linker = base_linker_wrapper.linker + libc_dev = yield Get(LibcDev, NativeToolchain, native_toolchain) + working_linker = Linker( + path_entries=(working_cpp_compiler.path_entries + base_linker.path_entries), + exe_filename=working_cpp_compiler.exe_filename, + library_dirs=(base_linker.library_dirs + working_cpp_compiler.library_dirs), + linking_library_dirs=(base_linker.linking_library_dirs + libc_dev.get_libc_dirs(platform)), + extra_args=base_linker.extra_args) + + yield GCCCppToolchain(CppToolchain(working_cpp_compiler, working_linker)) def create_native_toolchain_rules(): return [ - select_linker, - select_llvm_c_compiler, - select_llvm_cpp_compiler, - select_gcc_c_compiler, - select_gcc_cpp_compiler, - select_c_compiler, - select_cpp_compiler, + select_libc_dev, + select_assembler, + select_base_linker, + select_gcc_install_location, + select_llvm_c_toolchain, + select_llvm_cpp_toolchain, + select_gcc_c_toolchain, + select_gcc_cpp_toolchain, RootRule(NativeToolchain), ] diff --git a/src/python/pants/backend/native/subsystems/utils/BUILD b/src/python/pants/backend/native/subsystems/utils/BUILD index d269aa2feba..07d276f3466 100644 --- a/src/python/pants/backend/native/subsystems/utils/BUILD +++ b/src/python/pants/backend/native/subsystems/utils/BUILD @@ -1,5 +1,6 @@ python_library( dependencies=[ + 'src/python/pants/binaries', 'src/python/pants/subsystem', 'src/python/pants/util:dirutil', 'src/python/pants/util:memo', diff --git a/src/python/pants/backend/native/subsystems/utils/archive_file_mapper.py b/src/python/pants/backend/native/subsystems/utils/archive_file_mapper.py new file mode 100644 index 00000000000..c75d87d899d --- /dev/null +++ b/src/python/pants/backend/native/subsystems/utils/archive_file_mapper.py @@ -0,0 +1,66 @@ +# coding=utf-8 +# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import absolute_import, division, print_function, unicode_literals + +import glob +import os + +from pants.subsystem.subsystem import Subsystem +from pants.util.collections import assert_single_element + + +class ArchiveFileMapper(Subsystem): + """Index into known paths relative to a base directory. + + This is used with `NativeTool`s that wrap a compressed archive, which may have slightly different + paths across platforms. The helper methods from this class make it concise to express searching + for a single exact match for each of a set of directory path globs. + """ + + options_scope = 'archive-file-mapper' + + class ArchiveFileMappingError(Exception): pass + + def assert_single_path_by_glob(self, components): + """Assert that the path components (which are joined into a glob) match exactly one path. + + The matched path may be a file or a directory. This method is used to avoid having to guess + platform-specific intermediate directory names, e.g. 'x86_64-linux-gnu' or + 'x86_64-apple-darwin17.5.0'. + """ + glob_path_string = os.path.join(*components) + expanded_glob = glob.glob(glob_path_string) + + try: + return assert_single_element(expanded_glob) + except StopIteration as e: + raise self.ArchiveFileMappingError( + "No elements for glob '{}' -- expected exactly one." + .format(glob_path_string), + e) + except ValueError as e: + raise self.ArchiveFileMappingError( + "Should have exactly one path matching expansion of glob '{}'." + .format(glob_path_string), + e) + + def map_files(self, base_dir, all_components_list): + """Apply `assert_single_path_by_glob()` to all elements of `all_components_list`. + + Each element of `all_components_list` should be a tuple of path components, including + wildcards. The elements of each tuple are joined, and interpreted as a glob expression relative + to `base_dir`. The resulting glob should match exactly one path. + + :return: List of matched paths, one per element of `all_components_list`. + :raises: :class:`ArchiveFileMapper.ArchiveFileMappingError` if more or less than one path was + matched by one of the glob expressions interpreted from `all_components_list`. + """ + mapped_paths = [] + for components_tupled in all_components_list: + with_base = [base_dir] + list(components_tupled) + # Results are known to exist, since they match a glob. + mapped_paths.append(self.assert_single_path_by_glob(with_base)) + + return mapped_paths diff --git a/src/python/pants/backend/native/subsystems/utils/parse_search_dirs.py b/src/python/pants/backend/native/subsystems/utils/parse_search_dirs.py index 1129981ad5a..adbebe08a59 100644 --- a/src/python/pants/backend/native/subsystems/utils/parse_search_dirs.py +++ b/src/python/pants/backend/native/subsystems/utils/parse_search_dirs.py @@ -37,25 +37,31 @@ class ParseSearchDirsError(Exception): pass def _search_dirs_libraries_regex(cls): return re.compile('^libraries: =(.*)$', flags=re.MULTILINE) - def _parse_libraries_from_compiler_search_dirs(self, compiler_exe, env): - # This argument is supported by at least gcc and clang. - cmd = [compiler_exe, '-print-search-dirs'] - + def _invoke_compiler_exe(self, cmd, env): try: # Get stderr interspersed in the error message too -- this should not affect output parsing. compiler_output = subprocess.check_output(cmd, env=env, stderr=subprocess.STDOUT) except OSError as e: # We use `safe_shlex_join` here to pretty-print the command. raise self.ParseSearchDirsError( - "Invocation of '{}' with argv '{}' failed." - .format(compiler_exe, safe_shlex_join(cmd)), + "Process invocation with argv '{}' and environment {!r} failed." + .format(safe_shlex_join(cmd), env), e) except subprocess.CalledProcessError as e: raise self.ParseSearchDirsError( - "Invocation of '{}' with argv '{}' exited with non-zero code {}. output:\n{}" - .format(compiler_exe, safe_shlex_join(cmd), e.returncode, e.output), + "Process invocation with argv '{}' and environment {!r} exited with non-zero code {}. " + "output:\n{}" + .format(safe_shlex_join(cmd), env, e.returncode, e.output), e) + return compiler_output + + def _parse_libraries_from_compiler_search_dirs(self, compiler_exe, env): + # This argument is supported by at least gcc and clang. + cmd = [compiler_exe, '-print-search-dirs'] + + compiler_output = self._invoke_compiler_exe(cmd, env) + libs_line = self._search_dirs_libraries_regex.search(compiler_output) if not libs_line: raise self.ParseSearchDirsError( @@ -63,18 +69,20 @@ def _parse_libraries_from_compiler_search_dirs(self, compiler_exe, env): .format(safe_shlex_join(cmd), compiler_output)) return libs_line.group(1).split(':') - def get_compiler_library_dirs(self, compiler_exe, env=None): - all_dir_candidates = self._parse_libraries_from_compiler_search_dirs(compiler_exe, env=env) - - real_lib_dirs = OrderedSet() + def _filter_existing_dirs(self, dir_candidates, compiler_exe): + real_dirs = OrderedSet() - for lib_dir_path in all_dir_candidates: + for maybe_existing_dir in dir_candidates: # Could use a `seen_dir_paths` set if we want to avoid pinging the fs for duplicate entries. - if is_readable_dir(lib_dir_path): - real_lib_dirs.add(os.path.realpath(lib_dir_path)) + if is_readable_dir(maybe_existing_dir): + real_dirs.add(os.path.realpath(maybe_existing_dir)) else: - logger.debug("non-existent or non-accessible program directory at {} while " - "parsing libraries from {}" - .format(lib_dir_path, compiler_exe)) + logger.debug("non-existent or non-accessible directory at {} while " + "parsing directories from {}" + .format(maybe_existing_dir, compiler_exe)) + + return list(real_dirs) - return list(real_lib_dirs) + def get_compiler_library_dirs(self, compiler_exe, env=None): + all_dir_candidates = self._parse_libraries_from_compiler_search_dirs(compiler_exe, env=env) + return self._filter_existing_dirs(all_dir_candidates, compiler_exe) diff --git a/src/python/pants/backend/native/subsystems/xcode_cli_tools.py b/src/python/pants/backend/native/subsystems/xcode_cli_tools.py index 6288133d290..08bad5515cb 100644 --- a/src/python/pants/backend/native/subsystems/xcode_cli_tools.py +++ b/src/python/pants/backend/native/subsystems/xcode_cli_tools.py @@ -14,6 +14,12 @@ from pants.util.memo import memoized_method, memoized_property +MIN_OSX_SUPPORTED_VERSION = '10.11' + + +MIN_OSX_VERSION_ARG = '-mmacosx-version-min={}'.format(MIN_OSX_SUPPORTED_VERSION) + + class XCodeCLITools(Subsystem): """Subsystem to detect and provide the XCode command line developer tools. @@ -122,6 +128,7 @@ def include_dirs(self): all_inc_dirs = base_inc_dirs for d in base_inc_dirs: + # FIXME: what does this directory do? secure_inc_dir = os.path.join(d, 'secure') if is_readable_dir(secure_inc_dir): all_inc_dirs.append(secure_inc_dir) @@ -140,7 +147,9 @@ def linker(self): return Linker( path_entries=self.path_entries(), exe_filename='ld', - library_dirs=[]) + library_dirs=[], + linking_library_dirs=[], + extra_args=[MIN_OSX_VERSION_ARG]) @memoized_method def c_compiler(self): @@ -148,7 +157,8 @@ def c_compiler(self): path_entries=self.path_entries(), exe_filename='clang', library_dirs=self.lib_dirs(), - include_dirs=self.include_dirs()) + include_dirs=self.include_dirs(), + extra_args=[MIN_OSX_VERSION_ARG]) @memoized_method def cpp_compiler(self): @@ -156,7 +166,8 @@ def cpp_compiler(self): path_entries=self.path_entries(), exe_filename='clang++', library_dirs=self.lib_dirs(), - include_dirs=self.include_dirs()) + include_dirs=self.include_dirs(), + extra_args=[MIN_OSX_VERSION_ARG]) @rule(Assembler, [Select(XCodeCLITools)]) diff --git a/src/python/pants/backend/native/tasks/c_compile.py b/src/python/pants/backend/native/tasks/c_compile.py index 6d0c47aba8a..d962687a957 100644 --- a/src/python/pants/backend/native/tasks/c_compile.py +++ b/src/python/pants/backend/native/tasks/c_compile.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals -from pants.backend.native.config.environment import CCompiler +from pants.backend.native.config.environment import LLVMCToolchain from pants.backend.native.subsystems.native_compile_settings import CCompileSettings from pants.backend.native.subsystems.native_toolchain import NativeToolchain from pants.backend.native.targets.native_library import CLibrary @@ -32,16 +32,15 @@ def subsystem_dependencies(cls): ) @memoized_property - def _toolchain(self): + def _native_toolchain(self): return NativeToolchain.scoped_instance(self) def get_compile_settings(self): return CCompileSettings.scoped_instance(self) - def get_compiler(self): - return self._request_single(CCompiler, self._toolchain) + @memoized_property + def _c_toolchain(self): + return self._request_single(LLVMCToolchain, self._native_toolchain).c_toolchain - # FIXME(#5951): don't have any command-line args in the task or in the subsystem -- rather, - # subsystem options should be used to populate an `Executable` which produces its own arguments. - def extra_compile_args(self): - return ['-x', 'c', '-std=c11'] + def get_compiler(self): + return self._c_toolchain.c_compiler diff --git a/src/python/pants/backend/native/tasks/cpp_compile.py b/src/python/pants/backend/native/tasks/cpp_compile.py index 482c56084ac..322c62432c2 100644 --- a/src/python/pants/backend/native/tasks/cpp_compile.py +++ b/src/python/pants/backend/native/tasks/cpp_compile.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals -from pants.backend.native.config.environment import CppCompiler +from pants.backend.native.config.environment import LLVMCppToolchain from pants.backend.native.subsystems.native_compile_settings import CppCompileSettings from pants.backend.native.subsystems.native_toolchain import NativeToolchain from pants.backend.native.targets.native_library import CppLibrary @@ -32,27 +32,15 @@ def subsystem_dependencies(cls): ) @memoized_property - def _toolchain(self): + def _native_toolchain(self): return NativeToolchain.scoped_instance(self) def get_compile_settings(self): return CppCompileSettings.scoped_instance(self) + @memoized_property + def _cpp_toolchain(self): + return self._request_single(LLVMCppToolchain, self._native_toolchain).cpp_toolchain + def get_compiler(self): - return self._request_single(CppCompiler, self._toolchain) - - def _make_compile_argv(self, compile_request): - # FIXME: this is a temporary fix, do not do any of this kind of introspection. - # https://github.com/pantsbuild/pants/issues/5951 - prev_argv = super(CppCompile, self)._make_compile_argv(compile_request) - - if compile_request.compiler.exe_filename == 'clang++': - new_argv = [prev_argv[0], '-nobuiltininc', '-nostdinc++'] + prev_argv[1:] - else: - new_argv = prev_argv - return new_argv - - # FIXME(#5951): don't have any command-line args in the task or in the subsystem -- rather, - # subsystem options should be used to populate an `Executable` which produces its own arguments. - def extra_compile_args(self): - return ['-x', 'c++', '-std=c++11'] + return self._cpp_toolchain.cpp_compiler diff --git a/src/python/pants/backend/native/tasks/link_shared_libraries.py b/src/python/pants/backend/native/tasks/link_shared_libraries.py index 2d8e8e429c3..b4d528b5986 100644 --- a/src/python/pants/backend/native/tasks/link_shared_libraries.py +++ b/src/python/pants/backend/native/tasks/link_shared_libraries.py @@ -6,7 +6,7 @@ import os -from pants.backend.native.config.environment import Linker, Platform +from pants.backend.native.config.environment import LLVMCppToolchain, Platform from pants.backend.native.subsystems.native_toolchain import NativeToolchain from pants.backend.native.targets.native_library import NativeLibrary from pants.backend.native.tasks.native_compile import NativeTargetDependencies, ObjectFiles @@ -59,12 +59,16 @@ def subsystem_dependencies(cls): return super(LinkSharedLibraries, cls).subsystem_dependencies() + (NativeToolchain.scoped(cls),) @memoized_property - def _toolchain(self): + def _native_toolchain(self): return NativeToolchain.scoped_instance(self) + @memoized_property + def _cpp_toolchain(self): + return self._request_single(LLVMCppToolchain, self._native_toolchain).cpp_toolchain + @memoized_property def linker(self): - return self._request_single(Linker, self._toolchain) + return self._cpp_toolchain.cpp_linker def _retrieve_single_product_at_target_base(self, product_mapping, target): self.context.log.debug("product_mapping: {}".format(product_mapping)) @@ -97,6 +101,7 @@ def execute(self): # https://github.com/pantsbuild/pants/issues/6178 link_request = self._make_link_request( vt, compiled_objects_product, native_target_deps_product, external_libs_product) + self.context.log.debug("link_request: {}".format(link_request)) shared_library = self._execute_link_request(link_request) same_name_shared_lib = all_shared_libs_by_name.get(shared_library.name, None) @@ -148,13 +153,10 @@ def _make_link_request(self, external_libs_info=external_libs_product) _SHARED_CMDLINE_ARGS = { - 'darwin': lambda: ['-mmacosx-version-min=10.11', '-Wl,-dylib'], + 'darwin': lambda: ['-Wl,-dylib'], 'linux': lambda: ['-shared'], } - def _get_shared_lib_cmdline_args(self, platform): - return platform.resolve_platform_specific(self._SHARED_CMDLINE_ARGS) - def _execute_link_request(self, link_request): object_files = link_request.object_files @@ -169,10 +171,15 @@ def _execute_link_request(self, link_request): resulting_shared_lib_path = os.path.join(output_dir, native_artifact.as_shared_lib(platform)) # We are executing in the results_dir, so get absolute paths for everything. cmd = ([linker.exe_filename] + - self._get_shared_lib_cmdline_args(platform) + + platform.resolve_platform_specific(self._SHARED_CMDLINE_ARGS) + + linker.extra_args + link_request.external_libs_info.get_third_party_lib_args() + ['-o', os.path.abspath(resulting_shared_lib_path)] + [os.path.abspath(obj) for obj in object_files]) + self.context.log.debug("linker command: {}".format(cmd)) + + env = linker.as_invocation_environment_dict + self.context.log.debug("linker invocation environment: {}".format(env)) with self.context.new_workunit(name='link-shared-libraries', labels=[WorkUnitLabel.LINKER]) as workunit: @@ -182,19 +189,21 @@ def _execute_link_request(self, link_request): cwd=output_dir, stdout=workunit.output('stdout'), stderr=workunit.output('stderr'), - env=linker.get_invocation_environment_dict(platform)) + env=env) except OSError as e: workunit.set_outcome(WorkUnit.FAILURE) raise self.LinkSharedLibrariesError( - "Error invoking the native linker with command {} for request {}: {}" - .format(cmd, link_request, e), + "Error invoking the native linker with command {cmd} and environment {env} " + "for request {req}: {err}." + .format(cmd=cmd, env=env, req=link_request, err=e), e) rc = process.wait() if rc != 0: workunit.set_outcome(WorkUnit.FAILURE) raise self.LinkSharedLibrariesError( - "Error linking native objects with command {} for request {}. Exit code was: {}." - .format(cmd, link_request, rc)) + "Error linking native objects with command {cmd} and environment {env} " + "for request {req}. Exit code was: {rc}." + .format(cmd=cmd, env=env, req=link_request, rc=rc)) return SharedLibrary(name=native_artifact.lib_name, path=resulting_shared_lib_path) diff --git a/src/python/pants/backend/native/tasks/native_compile.py b/src/python/pants/backend/native/tasks/native_compile.py index 6a1e7ee826e..9d3b9e34099 100644 --- a/src/python/pants/backend/native/tasks/native_compile.py +++ b/src/python/pants/backend/native/tasks/native_compile.py @@ -9,7 +9,7 @@ from builtins import filter from collections import defaultdict -from pants.backend.native.config.environment import Executable, Platform +from pants.backend.native.config.environment import Executable from pants.backend.native.targets.native_library import NativeLibrary from pants.backend.native.tasks.native_external_library_fetch import NativeExternalLibraryFetch from pants.backend.native.tasks.native_task import NativeTask @@ -208,33 +208,23 @@ def _make_compile_request(self, versioned_target, dependencies, external_libs_pr 'fatal_warnings', target), output_dir=versioned_target.results_dir) - @abstractmethod - def extra_compile_args(self): - """Return a list of task-specific arguments to use to compile sources.""" - def _make_compile_argv(self, compile_request): """Return a list of arguments to use to compile sources. Subclasses can override and append.""" compiler = compile_request.compiler err_flags = ['-Werror'] if compile_request.fatal_warnings else [] - platform = Platform.create() - - platform_specific_flags = platform.resolve_platform_specific({ - 'linux': lambda: [], - 'darwin': lambda: ['-mmacosx-version-min=10.11'], - }) - # We are going to execute in the target output, so get absolute paths for everything. # TODO: If we need to produce static libs, don't add -fPIC! (could use Variants -- see #5788). argv = ( [compiler.exe_filename] + - platform_specific_flags + - self.extra_compile_args() + + compiler.extra_args + err_flags + ['-c', '-fPIC'] + ['-I{}'.format(os.path.abspath(inc_dir)) for inc_dir in compile_request.include_dirs] + [os.path.abspath(src) for src in compile_request.sources]) + self.context.log.debug("compile argv: {}".format(argv)) + return argv def _compile(self, compile_request): @@ -255,8 +245,7 @@ def _compile(self, compile_request): output_dir = compile_request.output_dir argv = self._make_compile_argv(compile_request) - - platform = Platform.create() + env = compiler.as_invocation_environment_dict with self.context.new_workunit( name=self.workunit_label, labels=[WorkUnitLabel.COMPILER]) as workunit: @@ -266,19 +255,20 @@ def _compile(self, compile_request): cwd=output_dir, stdout=workunit.output('stdout'), stderr=workunit.output('stderr'), - env=compiler.get_invocation_environment_dict(platform)) + env=env) except OSError as e: workunit.set_outcome(WorkUnit.FAILURE) raise self.NativeCompileError( - "Error invoking '{exe}' with command {cmd} for request {req}: {err}" - .format(exe=compiler.exe_filename, cmd=argv, req=compile_request, err=e)) + "Error invoking '{exe}' with command {cmd} and environment {env} for request {req}: {err}" + .format(exe=compiler.exe_filename, cmd=argv, env=env, req=compile_request, err=e)) rc = process.wait() if rc != 0: workunit.set_outcome(WorkUnit.FAILURE) raise self.NativeCompileError( - "Error in '{section_name}' with command {cmd} for request {req}. Exit code was: {rc}." - .format(section_name=self.workunit_label, cmd=argv, req=compile_request, rc=rc)) + "Error in '{section_name}' with command {cmd} and environment {env} for request {req}. " + "Exit code was: {rc}." + .format(section_name=self.workunit_label, cmd=argv, env=env, req=compile_request, rc=rc)) def collect_cached_objects(self, versioned_target): """Scan `versioned_target`'s results directory and return the output files from that directory. diff --git a/src/python/pants/backend/python/register.py b/src/python/pants/backend/python/register.py index 1eb887d0d97..0884e2e5ecc 100644 --- a/src/python/pants/backend/python/register.py +++ b/src/python/pants/backend/python/register.py @@ -28,7 +28,7 @@ from pants.backend.python.tasks.python_run import PythonRun from pants.backend.python.tasks.resolve_requirements import ResolveRequirements from pants.backend.python.tasks.select_interpreter import SelectInterpreter -from pants.backend.python.tasks.setup_py import SetupPy, create_setup_py_rules +from pants.backend.python.tasks.setup_py import SetupPy from pants.build_graph.build_file_aliases import BuildFileAliases from pants.build_graph.resources import Resources from pants.goal.task_registrar import TaskRegistrar as task @@ -71,7 +71,3 @@ def register_goals(): task(name='py-wheels', action=LocalPythonDistributionArtifact).install('binary') task(name='isort', action=IsortPythonTask).install('fmt') task(name='py', action=PythonBundle).install('bundle') - - -def rules(): - return create_setup_py_rules() diff --git a/src/python/pants/backend/python/subsystems/python_native_code.py b/src/python/pants/backend/python/subsystems/python_native_code.py index 01b3175e63d..6e57e20b35b 100644 --- a/src/python/pants/backend/python/subsystems/python_native_code.py +++ b/src/python/pants/backend/python/subsystems/python_native_code.py @@ -4,18 +4,25 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import os from builtins import str from collections import defaultdict +from wheel.install import WheelFile + +from pants.backend.native.config.environment import CppToolchain, CToolchain, Platform from pants.backend.native.subsystems.native_toolchain import NativeToolchain +from pants.backend.native.subsystems.xcode_cli_tools import MIN_OSX_VERSION_ARG from pants.backend.native.targets.native_library import NativeLibrary from pants.backend.python.subsystems.python_setup import PythonSetup from pants.backend.python.targets.python_binary import PythonBinary from pants.backend.python.targets.python_distribution import PythonDistribution +from pants.backend.python.tasks.pex_build_util import resolve_multi from pants.base.exceptions import IncompatiblePlatformsError from pants.subsystem.subsystem import Subsystem from pants.util.memo import memoized_property -from pants.util.objects import Exactly +from pants.util.objects import Exactly, datatype +from pants.util.strutil import create_path_env_var, safe_shlex_join class PythonNativeCode(Subsystem): @@ -129,3 +136,128 @@ def check_build_for_current_platform_only(self, targets): 'native code. Please ensure that the platform arguments in all relevant targets and build ' 'options are compatible with the current platform. Found targets for platforms: {}' .format(str(platforms_with_sources))) + + +class SetupPyNativeTools(datatype([ + ('c_toolchain', CToolchain), + ('cpp_toolchain', CppToolchain), + ('platform', Platform), +])): + """The native tools needed for a setup.py invocation. + + This class exists because `SetupPyExecutionEnvironment` is created manually, one per target. + """ + + +class SetupRequiresSiteDir(datatype(['site_dir'])): pass + + +# TODO: This could be formulated as an @rule if targets and `PythonInterpreter` are made available +# to the v2 engine. +def ensure_setup_requires_site_dir(reqs_to_resolve, interpreter, site_dir, + platforms=None): + if not reqs_to_resolve: + return None + + setup_requires_dists = resolve_multi(interpreter, reqs_to_resolve, platforms, None) + + # FIXME: there's no description of what this does or why it's necessary. + overrides = { + 'purelib': site_dir, + 'headers': os.path.join(site_dir, 'headers'), + 'scripts': os.path.join(site_dir, 'bin'), + 'platlib': site_dir, + 'data': site_dir + } + + # The `python_dist` target builds for the current platform only. + # FIXME: why does it build for the current platform only? + for obj in setup_requires_dists['current']: + wf = WheelFile(obj.location) + wf.install(overrides=overrides, force=True) + + return SetupRequiresSiteDir(site_dir) + + +# TODO: It might be pretty useful to have an Optional TypeConstraint. +class SetupPyExecutionEnvironment(datatype([ + # If None, don't set PYTHONPATH in the setup.py environment. + 'setup_requires_site_dir', + # If None, don't execute in the toolchain environment. + 'setup_py_native_tools', +])): + + _SHARED_CMDLINE_ARGS = { + 'darwin': lambda: [ + MIN_OSX_VERSION_ARG, + '-Wl,-dylib', + '-undefined', + 'dynamic_lookup', + ], + 'linux': lambda: ['-shared'], + } + + def as_environment(self): + ret = {} + + if self.setup_requires_site_dir: + ret['PYTHONPATH'] = self.setup_requires_site_dir.site_dir + + # FIXME(#5951): the below is a lot of error-prone repeated logic -- we need a way to compose + # executables more hygienically. We should probably be composing each datatype's members, and + # only creating an environment at the very end. + native_tools = self.setup_py_native_tools + if native_tools: + # An as_tuple() method for datatypes could make this destructuring cleaner! Alternatively, + # constructing this environment could be done more compositionally instead of requiring all of + # these disparate fields together at once. + plat = native_tools.platform + c_toolchain = native_tools.c_toolchain + c_compiler = c_toolchain.c_compiler + c_linker = c_toolchain.c_linker + + cpp_toolchain = native_tools.cpp_toolchain + cpp_compiler = cpp_toolchain.cpp_compiler + cpp_linker = cpp_toolchain.cpp_linker + + all_path_entries = ( + c_compiler.path_entries + + c_linker.path_entries + + cpp_compiler.path_entries + + cpp_linker.path_entries) + ret['PATH'] = create_path_env_var(all_path_entries) + + all_library_dirs = ( + c_compiler.library_dirs + + c_linker.library_dirs + + cpp_compiler.library_dirs + + cpp_linker.library_dirs) + joined_library_dirs = create_path_env_var(all_library_dirs) + dynamic_lib_env_var = plat.resolve_platform_specific({ + 'darwin': lambda: 'DYLD_LIBRARY_PATH', + 'linux': lambda: 'LD_LIBRARY_PATH', + }) + ret[dynamic_lib_env_var] = joined_library_dirs + + all_linking_library_dirs = (c_linker.linking_library_dirs + cpp_linker.linking_library_dirs) + ret['LIBRARY_PATH'] = create_path_env_var(all_linking_library_dirs) + + all_include_dirs = cpp_compiler.include_dirs + c_compiler.include_dirs + ret['CPATH'] = create_path_env_var(all_include_dirs) + + shared_compile_flags = safe_shlex_join(plat.resolve_platform_specific({ + 'darwin': lambda: [MIN_OSX_VERSION_ARG], + 'linux': lambda: [], + })) + ret['CFLAGS'] = shared_compile_flags + ret['CXXFLAGS'] = shared_compile_flags + + ret['CC'] = c_compiler.exe_filename + ret['CXX'] = cpp_compiler.exe_filename + ret['LDSHARED'] = cpp_linker.exe_filename + + all_new_ldflags = cpp_linker.extra_args + plat.resolve_platform_specific( + self._SHARED_CMDLINE_ARGS) + ret['LDFLAGS'] = safe_shlex_join(all_new_ldflags) + + return ret diff --git a/src/python/pants/backend/python/tasks/build_local_python_distributions.py b/src/python/pants/backend/python/tasks/build_local_python_distributions.py index 1991273a02a..f2eaf38bc2f 100644 --- a/src/python/pants/backend/python/tasks/build_local_python_distributions.py +++ b/src/python/pants/backend/python/tasks/build_local_python_distributions.py @@ -12,14 +12,17 @@ from pex import pep425tags from pex.interpreter import PythonInterpreter +from pants.backend.native.config.environment import LLVMCppToolchain, LLVMCToolchain, Platform from pants.backend.native.targets.native_library import NativeLibrary from pants.backend.native.tasks.link_shared_libraries import SharedLibrary from pants.backend.python.python_requirement import PythonRequirement -from pants.backend.python.subsystems.python_native_code import PythonNativeCode +from pants.backend.python.subsystems.python_native_code import (PythonNativeCode, + SetupPyExecutionEnvironment, + SetupPyNativeTools, + ensure_setup_requires_site_dir) from pants.backend.python.targets.python_requirement_library import PythonRequirementLibrary from pants.backend.python.tasks.pex_build_util import is_local_python_dist -from pants.backend.python.tasks.setup_py import (SetupPyExecutionEnvironment, SetupPyNativeTools, - SetupPyRunner, ensure_setup_requires_site_dir) +from pants.backend.python.tasks.setup_py import SetupPyRunner from pants.base.build_environment import get_buildroot from pants.base.exceptions import TargetDefinitionException, TaskError from pants.build_graph.address import Address @@ -27,7 +30,7 @@ from pants.util.collections import assert_single_element from pants.util.contextutil import environment_as from pants.util.dirutil import safe_mkdir_for, split_basename_and_dirname -from pants.util.memo import memoized_property +from pants.util.memo import memoized_classproperty, memoized_property class BuildLocalPythonDistributions(Task): @@ -67,6 +70,12 @@ def subsystem_dependencies(cls): PythonNativeCode.scoped(cls), ) + class BuildLocalPythonDistributionsError(TaskError): pass + + @memoized_classproperty + def _platform(cls): + return Platform.create() + @memoized_property def _python_native_code_settings(self): return PythonNativeCode.scoped_instance(self) @@ -78,9 +87,16 @@ def _request_single(self, product, subject): return self.context._scheduler.product_request(product, [subject])[0] @memoized_property - def _setup_py_native_tools(self): - native_toolchain = self._python_native_code_settings.native_toolchain - return self._request_single(SetupPyNativeTools, native_toolchain) + def _c_toolchain(self): + llvm_c_toolchain = self._request_single( + LLVMCToolchain, self._python_native_code_settings.native_toolchain) + return llvm_c_toolchain.c_toolchain + + @memoized_property + def _cpp_toolchain(self): + llvm_cpp_toolchain = self._request_single( + LLVMCppToolchain, self._python_native_code_settings.native_toolchain) + return llvm_cpp_toolchain.cpp_toolchain # TODO: This should probably be made into an @classproperty (see PR #5901). @property @@ -196,7 +212,10 @@ def _prepare_and_create_dist(self, interpreter, shared_libs_product, versioned_t if self._python_native_code_settings.pydist_has_native_sources(dist_target): # We add the native tools if we need to compile code belonging to this python_dist() target. # TODO: test this branch somehow! - native_tools = self._setup_py_native_tools + native_tools = SetupPyNativeTools( + c_toolchain=self._c_toolchain, + cpp_toolchain=self._cpp_toolchain, + platform=self._platform) # Native code in this python_dist() target requires marking the dist as platform-specific. is_platform_specific = True elif len(all_native_artifacts) > 0: @@ -266,9 +285,24 @@ def _create_dist(self, dist_tgt, dist_target_dir, interpreter, setup_command=setup_py_snapshot_version_argv, interpreter=interpreter) - with environment_as(**setup_py_execution_environment.as_environment()): + setup_py_env = setup_py_execution_environment.as_environment() + with environment_as(**setup_py_env): # Build a whl using SetupPyRunner and return its absolute path. - setup_runner.run() + was_installed_successfully = setup_runner.run() + # FIXME: Make a run_raising_error() method in SetupPyRunner that doesn't print directly to + # stderr like pex does (better: put this in pex itself). + if not was_installed_successfully: + raise self.BuildLocalPythonDistributionsError( + "Installation of python distribution from target {target} into directory {into_dir} " + "failed.\n" + "The chosen interpreter was: {interpreter}.\n" + "The execution environment was: {env}.\n" + "The setup command was: {command}." + .format(target=dist_tgt, + into_dir=dist_target_dir, + interpreter=interpreter, + env=setup_py_env, + command=setup_py_snapshot_version_argv)) def _inject_synthetic_dist_requirements(self, dist, req_lib_addr): """Inject a synthetic requirements library that references a local wheel. @@ -290,8 +324,10 @@ def _get_whl_from_dir(cls, install_dir): dist_dir = cls._get_dist_dir(install_dir) dists = glob.glob(os.path.join(dist_dir, '*.whl')) if len(dists) == 0: - raise TaskError('No distributions were produced by python_create_distribution task.') + raise cls.BuildLocalPythonDistributionsError( + 'No distributions were produced by python_create_distribution task.') if len(dists) > 1: # TODO: is this ever going to happen? - raise TaskError('Ambiguous local python distributions found: {}'.format(dists)) + raise cls.BuildLocalPythonDistributionsError('Ambiguous local python distributions found: {}' + .format(dists)) return dists[0] diff --git a/src/python/pants/backend/python/tasks/setup_py.py b/src/python/pants/backend/python/tasks/setup_py.py index 0a875217445..8c76453a253 100644 --- a/src/python/pants/backend/python/tasks/setup_py.py +++ b/src/python/pants/backend/python/tasks/setup_py.py @@ -18,27 +18,22 @@ from pex.interpreter import PythonInterpreter from twitter.common.collections import OrderedSet from twitter.common.dirutil.chroot import Chroot -from wheel.install import WheelFile -from pants.backend.native.config.environment import CCompiler, CppCompiler, Linker, Platform from pants.backend.python.targets.python_binary import PythonBinary from pants.backend.python.targets.python_requirement_library import PythonRequirementLibrary from pants.backend.python.targets.python_target import PythonTarget -from pants.backend.python.tasks.pex_build_util import is_local_python_dist, resolve_multi +from pants.backend.python.tasks.pex_build_util import is_local_python_dist from pants.base.build_environment import get_buildroot from pants.base.exceptions import TargetDefinitionException, TaskError from pants.base.specs import SiblingAddresses from pants.build_graph.address_lookup_error import AddressLookupError from pants.build_graph.build_graph import sort_targets from pants.build_graph.resources import Resources -from pants.engine.rules import rule -from pants.engine.selectors import Select from pants.task.task import Task from pants.util.dirutil import safe_rmtree, safe_walk from pants.util.memo import memoized_property from pants.util.meta import AbstractClass -from pants.util.objects import datatype -from pants.util.strutil import create_path_env_var, safe_shlex_join, safe_shlex_split +from pants.util.strutil import safe_shlex_split SETUP_BOILERPLATE = """ @@ -80,125 +75,6 @@ def _setup_command(self): return self.__setup_command -class SetupPyNativeTools(datatype([ - ('c_compiler', CCompiler), - ('cpp_compiler', CppCompiler), - ('linker', Linker), - ('platform', Platform), -])): - """The native tools needed for a setup.py invocation. - - This class exists because `SetupPyExecutionEnvironment` is created manually, one per target. - """ - - -@rule(SetupPyNativeTools, [Select(CCompiler), Select(CppCompiler), Select(Linker), Select(Platform)]) -def get_setup_py_native_tools(c_compiler, cpp_compiler, linker, platform): - yield SetupPyNativeTools( - c_compiler=c_compiler, - cpp_compiler=cpp_compiler, - linker=linker, - platform=platform) - - -class SetupRequiresSiteDir(datatype(['site_dir'])): pass - - -# TODO: This could be formulated as an @rule if targets and `PythonInterpreter` are made available -# to the v2 engine. -def ensure_setup_requires_site_dir(reqs_to_resolve, interpreter, site_dir, - platforms=None): - if not reqs_to_resolve: - return None - - setup_requires_dists = resolve_multi(interpreter, reqs_to_resolve, platforms, None) - - # FIXME: there's no description of what this does or why it's necessary. - overrides = { - 'purelib': site_dir, - 'headers': os.path.join(site_dir, 'headers'), - 'scripts': os.path.join(site_dir, 'bin'), - 'platlib': site_dir, - 'data': site_dir - } - - # The `python_dist` target builds for the current platform only. - # FIXME: why does it build for the current platform only? - for obj in setup_requires_dists['current']: - wf = WheelFile(obj.location) - wf.install(overrides=overrides, force=True) - - return SetupRequiresSiteDir(site_dir) - - -class SetupPyExecutionEnvironment(datatype([ - # TODO: It might be pretty useful to have an Optional TypeConstraint. - 'setup_requires_site_dir', - # If None, don't execute in the toolchain environment. - 'setup_py_native_tools', -])): - - _SHARED_CMDLINE_ARGS = { - 'darwin': lambda: [ - '-mmacosx-version-min=10.11', - '-Wl,-dylib', - '-undefined', - 'dynamic_lookup', - ], - 'linux': lambda: ['-shared'], - } - - def as_environment(self): - ret = {} - - if self.setup_requires_site_dir: - ret['PYTHONPATH'] = self.setup_requires_site_dir.site_dir - - # FIXME(#5951): the below is a lot of error-prone repeated logic -- we need a way to compose - # executables more hygienically. We should probably be composing each datatype's members, and - # only creating an environment at the very end. - native_tools = self.setup_py_native_tools - if native_tools: - # TODO: an as_tuple() method for datatypes would make this destructuring cleaner! - plat = native_tools.platform - cc = native_tools.c_compiler - cxx = native_tools.cpp_compiler - linker = native_tools.linker - - all_path_entries = cc.path_entries + cxx.path_entries + linker.path_entries - ret['PATH'] = create_path_env_var(all_path_entries) - - all_library_dirs = cc.library_dirs + cxx.library_dirs + linker.library_dirs - if all_library_dirs: - joined_library_dirs = create_path_env_var(all_library_dirs) - ret['LIBRARY_PATH'] = joined_library_dirs - dynamic_lib_env_var = plat.resolve_platform_specific({ - 'darwin': lambda: 'DYLD_LIBRARY_PATH', - 'linux': lambda: 'LD_LIBRARY_PATH', - }) - ret[dynamic_lib_env_var] = joined_library_dirs - - all_include_dirs = cc.include_dirs + cxx.include_dirs - if all_include_dirs: - ret['CPATH'] = create_path_env_var(all_include_dirs) - - all_cflags_for_platform = plat.resolve_platform_specific({ - 'darwin': lambda: ['-mmacosx-version-min=10.11'], - 'linux': lambda: [], - }) - if all_cflags_for_platform: - ret['CFLAGS'] = safe_shlex_join(all_cflags_for_platform) - - ret['CC'] = cc.exe_filename - ret['CXX'] = cxx.exe_filename - ret['LDSHARED'] = linker.exe_filename - - all_new_ldflags = plat.resolve_platform_specific(self._SHARED_CMDLINE_ARGS) - ret['LDFLAGS'] = safe_shlex_join(all_new_ldflags) - - return ret - - class TargetAncestorIterator(object): """Supports iteration of target ancestor lineages.""" @@ -766,9 +642,3 @@ def create(exported_python_target): setup_runner = SetupPyRunner(setup_dir, split_command, interpreter=interpreter) setup_runner.run() python_dists[exported_python_target] = setup_dir - - -def create_setup_py_rules(): - return [ - get_setup_py_native_tools, - ] diff --git a/tests/python/pants_test/backend/native/subsystems/test_libc_resolution.py b/tests/python/pants_test/backend/native/subsystems/test_libc_resolution.py index 5a132b40af0..9b7f15d581c 100644 --- a/tests/python/pants_test/backend/native/subsystems/test_libc_resolution.py +++ b/tests/python/pants_test/backend/native/subsystems/test_libc_resolution.py @@ -74,6 +74,6 @@ def test_libc_compiler_search_failure(self): with self.assertRaises(ParseSearchDirs.ParseSearchDirsError) as cm: self.libc.get_libc_dirs(self.platform) expected_msg = ( - "Invocation of 'this_executable_does_not_exist' with argv " - "'this_executable_does_not_exist -print-search-dirs' failed.") + "Process invocation with argv " + "'this_executable_does_not_exist -print-search-dirs' and environment None failed.") self.assertIn(expected_msg, str(cm.exception)) diff --git a/tests/python/pants_test/backend/native/subsystems/test_native_toolchain.py b/tests/python/pants_test/backend/native/subsystems/test_native_toolchain.py index a594a8620dd..1992f19dab0 100644 --- a/tests/python/pants_test/backend/native/subsystems/test_native_toolchain.py +++ b/tests/python/pants_test/backend/native/subsystems/test_native_toolchain.py @@ -8,8 +8,8 @@ import re from contextlib import contextmanager -from pants.backend.native.config.environment import (GCCCCompiler, GCCCppCompiler, Linker, - LLVMCCompiler, LLVMCppCompiler, Platform) +from pants.backend.native.config.environment import (GCCCppToolchain, GCCCToolchain, + LLVMCppToolchain, LLVMCToolchain, Platform) from pants.backend.native.register import rules as native_backend_rules from pants.backend.native.subsystems.binaries.gcc import GCC from pants.backend.native.subsystems.binaries.llvm import LLVM @@ -18,7 +18,7 @@ from pants.util.contextutil import environment_as, pushd, temporary_dir from pants.util.dirutil import is_executable, safe_open from pants.util.process_handler import subprocess -from pants.util.strutil import create_path_env_var, safe_shlex_join +from pants.util.strutil import safe_shlex_join from pants_test.engine.scheduler_test_base import SchedulerTestBase from pants_test.subsystem.subsystem_util import global_subsystem_instance, init_subsystems from pants_test.test_base import TestBase @@ -39,69 +39,72 @@ def setUp(self): self.toolchain = global_subsystem_instance(NativeToolchain) self.rules = native_backend_rules() + gcc_subsystem = global_subsystem_instance(GCC) + self.gcc_version = gcc_subsystem.version() + llvm_subsystem = global_subsystem_instance(LLVM) + self.llvm_version = llvm_subsystem.version() + def _sched(self, *args, **kwargs): return self.mk_scheduler(rules=self.rules, *args, **kwargs) def test_gcc_version(self): scheduler = self._sched() + gcc_c_toolchain = self.execute_expecting_one_result( + scheduler, GCCCToolchain, self.toolchain).value - platform = Platform.create() - - gcc_subsystem = global_subsystem_instance(GCC) - gcc_version = gcc_subsystem.version() - - gcc_c_compiler = self.execute_expecting_one_result( - scheduler, GCCCCompiler, self.toolchain).value - gcc = gcc_c_compiler.c_compiler + gcc = gcc_c_toolchain.c_toolchain.c_compiler gcc_version_out = self._invoke_capturing_output( [gcc.exe_filename, '--version'], - env=gcc.get_invocation_environment_dict(platform)) + env=gcc.as_invocation_environment_dict) - gcc_version_regex = re.compile('^gcc.*{}$'.format(re.escape(gcc_version)), + gcc_version_regex = re.compile('^gcc.*{}$'.format(re.escape(self.gcc_version)), flags=re.MULTILINE) self.assertIsNotNone(gcc_version_regex.search(gcc_version_out)) - gcc_cpp_compiler = self.execute_expecting_one_result( - scheduler, GCCCppCompiler, self.toolchain).value - gpp = gcc_cpp_compiler.cpp_compiler + def test_gpp_version(self): + scheduler = self._sched() + gcc_cpp_toolchain = self.execute_expecting_one_result( + scheduler, GCCCppToolchain, self.toolchain).value + + gpp = gcc_cpp_toolchain.cpp_toolchain.cpp_compiler gpp_version_out = self._invoke_capturing_output( [gpp.exe_filename, '--version'], - env=gpp.get_invocation_environment_dict(platform)) + env=gpp.as_invocation_environment_dict) - gpp_version_regex = re.compile(r'^g\+\+.*{}$'.format(re.escape(gcc_version)), + gpp_version_regex = re.compile(r'^g\+\+.*{}$'.format(re.escape(self.gcc_version)), flags=re.MULTILINE) self.assertIsNotNone(gpp_version_regex.search(gpp_version_out)) def test_clang_version(self): scheduler = self._sched() + llvm_c_toolchain = self.execute_expecting_one_result( + scheduler, LLVMCToolchain, self.toolchain).value - platform = Platform.create() - - llvm_subsystem = global_subsystem_instance(LLVM) - llvm_version = llvm_subsystem.version() - llvm_version_regex = re.compile('^clang version {}'.format(re.escape(llvm_version)), - flags=re.MULTILINE) - - llvm_c_compiler = self.execute_expecting_one_result( - scheduler, LLVMCCompiler, self.toolchain).value - clang = llvm_c_compiler.c_compiler - llvm_version_out = self._invoke_capturing_output( + clang = llvm_c_toolchain.c_toolchain.c_compiler + clang_version_out = self._invoke_capturing_output( [clang.exe_filename, '--version'], - env=clang.get_invocation_environment_dict(platform)) + env=clang.as_invocation_environment_dict) - self.assertIsNotNone(llvm_version_regex.search(llvm_version_out)) + clang_version_regex = re.compile('^clang version {}'.format(re.escape(self.llvm_version)), + flags=re.MULTILINE) + self.assertIsNotNone(clang_version_regex.search(clang_version_out)) - llvm_cpp_compiler = self.execute_expecting_one_result( - scheduler, LLVMCppCompiler, self.toolchain).value - clangpp = llvm_cpp_compiler.cpp_compiler - gpp_version_out = self._invoke_capturing_output( + def test_clangpp_version(self): + scheduler = self._sched() + clangpp_version_regex = re.compile('^clang version {}'.format(re.escape(self.llvm_version)), + flags=re.MULTILINE) + + llvm_cpp_toolchain = self.execute_expecting_one_result( + scheduler, LLVMCppToolchain, self.toolchain).value + clangpp = llvm_cpp_toolchain.cpp_toolchain.cpp_compiler + clanggpp_version_out = self._invoke_capturing_output( [clangpp.exe_filename, '--version'], - env=clangpp.get_invocation_environment_dict(platform)) + env=clangpp.as_invocation_environment_dict) - self.assertIsNotNone(llvm_version_regex.search(gpp_version_out)) + self.assertIsNotNone(clangpp_version_regex.search(clanggpp_version_out)) @contextmanager - def _hello_world_source_environment(self, file_name, contents, scheduler_request_specs): + def _hello_world_source_environment(self, toolchain_type, file_name, contents): with temporary_dir() as tmpdir: scheduler = self._sched(work_dir=tmpdir) @@ -109,22 +112,22 @@ def _hello_world_source_environment(self, file_name, contents, scheduler_request with safe_open(source_file_path, mode='wb') as fp: fp.write(contents) - execution_request = scheduler.execution_request_literal(scheduler_request_specs) + toolchain = self.execute_expecting_one_result(scheduler, toolchain_type, self.toolchain).value with pushd(tmpdir): - yield tuple(self.execute_literal(scheduler, execution_request)) + yield toolchain def _invoke_compiler(self, compiler, args): - cmd = [compiler.exe_filename] + args + cmd = [compiler.exe_filename] + compiler.extra_args + args return self._invoke_capturing_output( cmd, - compiler.get_invocation_environment_dict(self.platform)) + compiler.as_invocation_environment_dict) def _invoke_linker(self, linker, args): - cmd = [linker.exe_filename] + args + cmd = [linker.exe_filename] + linker.extra_args + args return self._invoke_capturing_output( cmd, - linker.get_invocation_environment_dict(self.platform)) + linker.as_invocation_environment_dict) def _invoke_capturing_output(self, cmd, env=None): if env is None: @@ -145,136 +148,79 @@ def _invoke_capturing_output(self, cmd, env=None): out=e.output), e) - def _do_compile_link(self, compiler, linker, source_file, outfile, output, - extra_compile_args=None, extra_link_args=None, - extra_invocation_env=None): + def _do_compile_link(self, compiler, linker, source_file, outfile, output): intermediate_obj_file_name = '{}.o'.format(outfile) self._invoke_compiler( compiler, - ['-c', source_file, '-o', intermediate_obj_file_name] + (extra_compile_args or [])) + ['-c', source_file, '-o', intermediate_obj_file_name]) self.assertTrue(os.path.isfile(intermediate_obj_file_name)) self._invoke_linker( linker, - [intermediate_obj_file_name, '-o', outfile] + (extra_link_args or [])) + [intermediate_obj_file_name, '-o', outfile]) self.assertTrue(is_executable(outfile)) - program_out = self._invoke_capturing_output([os.path.abspath(outfile)], - env=extra_invocation_env) + + program_out = self._invoke_capturing_output([os.path.abspath(outfile)]) self.assertEqual((output + '\n'), program_out) def test_hello_c_gcc(self): - scheduler_request_specs = [ - (self.toolchain, GCCCCompiler), - (self.toolchain, Linker), - ] - - with self._hello_world_source_environment('hello.c', contents=""" + with self._hello_world_source_environment(GCCCToolchain, 'hello.c', contents=""" #include "stdio.h" int main() { printf("%s\\n", "I C the world!"); } -""", scheduler_request_specs=scheduler_request_specs) as products: +""") as gcc_c_toolchain: - gcc_wrapper, linker = products - gcc = gcc_wrapper.c_compiler + c_toolchain = gcc_c_toolchain.c_toolchain + compiler = c_toolchain.c_compiler + linker = c_toolchain.c_linker - self._do_compile_link(gcc, linker, 'hello.c', 'hello_gcc', "I C the world!") + self._do_compile_link(compiler, linker, 'hello.c', 'hello_gcc', "I C the world!") def test_hello_c_clang(self): - - scheduler_request_specs = [ - (self.toolchain, LLVMCCompiler), - (self.toolchain, Linker), - ] - - with self._hello_world_source_environment('hello.c', contents=""" + with self._hello_world_source_environment(LLVMCToolchain, 'hello.c', contents=""" #include "stdio.h" int main() { printf("%s\\n", "I C the world!"); } -""", scheduler_request_specs=scheduler_request_specs) as products: +""") as llvm_c_toolchain: - clang_wrapper, linker = products - clang = clang_wrapper.c_compiler + c_toolchain = llvm_c_toolchain.c_toolchain + compiler = c_toolchain.c_compiler + linker = c_toolchain.c_linker - self._do_compile_link(clang, linker, 'hello.c', 'hello_clang', "I C the world!") + self._do_compile_link(compiler, linker, 'hello.c', 'hello_clang', "I C the world!") def test_hello_cpp_gpp(self): - - scheduler_request_specs = [ - (self.toolchain, GCCCppCompiler), - (self.toolchain, LLVMCppCompiler), - (self.toolchain, Linker), - ] - - with self._hello_world_source_environment('hello.cpp', contents=""" + with self._hello_world_source_environment(GCCCppToolchain, 'hello.cpp', contents=""" #include int main() { std::cout << "I C the world, ++ more!" << std::endl; } -""", scheduler_request_specs=scheduler_request_specs) as products: - - gpp_wrapper, clangpp_wrapper, linker = products - gpp = gpp_wrapper.cpp_compiler - clangpp = clangpp_wrapper.cpp_compiler +""") as gcc_cpp_toolchain: - # FIXME(#5951): we should be matching the linker to the compiler here, instead of trying to - # use the same linker for everything. This is a temporary workaround. - linker_with_gpp_workaround = Linker( - path_entries=(gpp.path_entries + linker.path_entries), - exe_filename=gpp.exe_filename, - library_dirs=(gpp.library_dirs + linker.library_dirs + clangpp.library_dirs)) + cpp_toolchain = gcc_cpp_toolchain.cpp_toolchain + compiler = cpp_toolchain.cpp_compiler + linker = cpp_toolchain.cpp_linker - self._do_compile_link(gpp, linker_with_gpp_workaround, 'hello.cpp', 'hello_gpp', "I C the world, ++ more!") + self._do_compile_link(compiler, linker, 'hello.cpp', 'hello_gpp', "I C the world, ++ more!") def test_hello_cpp_clangpp(self): - - scheduler_request_specs = [ - # We need GCC to provide libstdc++.so.6, which clang needs to run on Linux. - (self.toolchain, GCCCppCompiler), - (self.toolchain, LLVMCppCompiler), - (self.toolchain, Linker), - ] - - with self._hello_world_source_environment('hello.cpp', contents=""" + with self._hello_world_source_environment(LLVMCppToolchain, 'hello.cpp', contents=""" #include int main() { std::cout << "I C the world, ++ more!" << std::endl; } -""", scheduler_request_specs=scheduler_request_specs) as products: - - gpp_wrapper, clangpp_wrapper, linker = products - gpp = gpp_wrapper.cpp_compiler - clangpp = clangpp_wrapper.cpp_compiler - - # FIXME(#5951): we should be matching the linker to the compiler here, instead of trying to - # use the same linker for everything. This is a temporary workaround. - linker_with_clangpp_workaround = Linker( - path_entries=(clangpp.path_entries + linker.path_entries), - exe_filename=clangpp.exe_filename, - library_dirs=(gpp.library_dirs + linker.library_dirs + clangpp.library_dirs)) - - lib_path_var = self.platform.resolve_platform_specific({ - 'darwin': lambda: 'DYLD_LIBRARY_PATH', - 'linux': lambda: 'LD_LIBRARY_PATH', - }) - runtime_libs_path = {lib_path_var: create_path_env_var(clangpp.library_dirs)} - self._do_compile_link( - clangpp, linker_with_clangpp_workaround, 'hello.cpp', 'hello_clangpp', - "I C the world, ++ more!", - # Otherwise we get some header errors on Linux because clang++ will prefer the system - # headers if they are allowed, and we provide our own already in the LLVM subsystem (and - # pass them in through CPATH). - extra_compile_args=['-nostdinc++'], - # LLVM will prefer LLVM's libc++ on OSX, and seemingly requires it even if it does not use - # its own C++ library implementation, and uses libstdc++, which we provide in the linker's - # LIBRARY_PATH. See https://libcxx.llvm.org/ for more info. - extra_link_args=['-lc++'], - # We need to provide libc++ on the runtime library path as well on Linux (OSX will have it - # already). - extra_invocation_env=runtime_libs_path) +""") as llvm_cpp_toolchain: + + cpp_toolchain = llvm_cpp_toolchain.cpp_toolchain + compiler = cpp_toolchain.cpp_compiler + linker = cpp_toolchain.cpp_linker + + self._do_compile_link(compiler, linker, 'hello.cpp', 'hello_clangpp', + "I C the world, ++ more!") diff --git a/tests/python/pants_test/backend/python/tasks/test_build_local_python_distributions.py b/tests/python/pants_test/backend/python/tasks/test_build_local_python_distributions.py index 5c2e600383c..7e264aa3859 100644 --- a/tests/python/pants_test/backend/python/tasks/test_build_local_python_distributions.py +++ b/tests/python/pants_test/backend/python/tasks/test_build_local_python_distributions.py @@ -9,7 +9,6 @@ from textwrap import dedent from pants.backend.native.register import rules as native_backend_rules -from pants.backend.python.register import rules as python_backend_rules from pants.backend.python.targets.python_distribution import PythonDistribution from pants.backend.python.tasks.build_local_python_distributions import \ BuildLocalPythonDistributions @@ -100,11 +99,7 @@ def _all_dist_targets(self): return list(self.target_dict.values()) def _scheduling_context(self, **kwargs): - rules = ( - native_backend_rules() + - python_backend_rules() - ) - scheduler = self.mk_scheduler(rules=rules) + scheduler = self.mk_scheduler(rules=native_backend_rules()) return self.context(scheduler=scheduler, **kwargs) def _retrieve_single_product_at_target_base(self, product_mapping, target):