Skip to content

Commit 4aec12c

Browse files
CMLivingstoncosmicexplorer
authored andcommitted
Conan (third party) support for ctypes native libraries (#5998)
### Problem The new targets introduced in #5815 have no means of declaring, fetching, and using third party native dependencies. ### Solution Integrate the Conan package manager to fetch packages from a remote package store via a new task, copy the package data to the results directory of `third_party_native_library` targets in play, and create a product that the compile and link tasks can consume. Plumb the directory paths provided by the product through to the command lines of the compile and link steps. ### Result Users can now depend on third party native libraries in their `ctypes_compatible_cpp_library` targets from either conan-center or a remote URI that they specify via an option. ## Some notes - The conan home directory is currently under .pants.d, instead of ~ or ~/.cache/pants. I did this for the short term to make debugging cache problems and other issues as simple as a ./pants clean-all. I don't think the perf loss will be too bad (1-2 seconds). - Some string manipulation could be better done as regex, and I have TODOs for that - I am going to follow up with upstream Conan about getting a flag for cleaner client output so the parse method does not need to be so ugly.
1 parent d3f2005 commit 4aec12c

File tree

20 files changed

+836
-45
lines changed

20 files changed

+836
-45
lines changed

src/python/pants/backend/native/register.py

+4
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
from pants.backend.native.subsystems.binaries.llvm import create_llvm_rules
1111
from pants.backend.native.subsystems.native_toolchain import create_native_toolchain_rules
1212
from pants.backend.native.subsystems.xcode_cli_tools import create_xcode_cli_tools_rules
13+
from pants.backend.native.targets.external_native_library import ExternalNativeLibrary
1314
from pants.backend.native.targets.native_artifact import NativeArtifact
1415
from pants.backend.native.targets.native_library import CLibrary, CppLibrary
1516
from pants.backend.native.tasks.c_compile import CCompile
1617
from pants.backend.native.tasks.cpp_compile import CppCompile
1718
from pants.backend.native.tasks.link_shared_libraries import LinkSharedLibraries
19+
from pants.backend.native.tasks.native_external_library_fetch import NativeExternalLibraryFetch
1820
from pants.build_graph.build_file_aliases import BuildFileAliases
1921
from pants.goal.task_registrar import TaskRegistrar as task
2022

@@ -24,6 +26,7 @@ def build_file_aliases():
2426
targets={
2527
CLibrary.alias(): CLibrary,
2628
CppLibrary.alias(): CppLibrary,
29+
ExternalNativeLibrary.alias(): ExternalNativeLibrary,
2730
},
2831
objects={
2932
NativeArtifact.alias(): NativeArtifact,
@@ -34,6 +37,7 @@ def build_file_aliases():
3437
def register_goals():
3538
# FIXME(#5962): register these under the 'compile' goal when we eliminate the product transitive
3639
# dependency from export -> compile.
40+
task(name='native-third-party-fetch', action=NativeExternalLibraryFetch).install('native-compile')
3741
task(name='c-for-ctypes', action=CCompile).install('native-compile')
3842
task(name='cpp-for-ctypes', action=CppCompile).install('native-compile')
3943
task(name='shared-libraries', action=LinkSharedLibraries).install('link')

src/python/pants/backend/native/subsystems/binaries/llvm.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pants.engine.rules import RootRule, rule
1414
from pants.engine.selectors import Select
1515
from pants.util.dirutil import is_readable_dir
16-
from pants.util.memo import memoized_method
16+
from pants.util.memo import memoized_method, memoized_property
1717

1818

1919
class LLVMReleaseUrlGenerator(BinaryToolUrlGenerator):
@@ -70,19 +70,34 @@ def linker(self, platform):
7070
self._PLATFORM_SPECIFIC_LINKER_NAME),
7171
library_dirs=[])
7272

73+
# FIXME: use ParseSearchDirs for this and other include directories -- we shouldn't be trying to
74+
# guess the path here.
75+
# https://github.com/pantsbuild/pants/issues/6143
76+
@memoized_property
77+
def _common_include_dirs(self):
78+
return [os.path.join(self.select(), 'lib/clang', self.version(), 'include')]
79+
80+
@memoized_property
81+
def _common_lib_dirs(self):
82+
return [os.path.join(self.select(), 'lib')]
83+
7384
def c_compiler(self):
7485
return CCompiler(
7586
path_entries=self.path_entries(),
7687
exe_filename='clang',
77-
library_dirs=[],
78-
include_dirs=[])
88+
library_dirs=self._common_lib_dirs,
89+
include_dirs=self._common_include_dirs)
90+
91+
@memoized_property
92+
def _cpp_include_dirs(self):
93+
return [os.path.join(self.select(), 'include/c++/v1')]
7994

8095
def cpp_compiler(self):
8196
return CppCompiler(
8297
path_entries=self.path_entries(),
8398
exe_filename='clang++',
84-
library_dirs=[],
85-
include_dirs=[])
99+
library_dirs=self._common_lib_dirs,
100+
include_dirs=(self._cpp_include_dirs + self._common_include_dirs))
86101

87102

88103
# FIXME(#5663): use this over the XCode linker!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# coding=utf-8
2+
# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
3+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
4+
5+
from __future__ import absolute_import, division, print_function, unicode_literals
6+
7+
import logging
8+
import os
9+
10+
from pex.interpreter import PythonInterpreter
11+
from pex.pex import PEX
12+
from pex.pex_builder import PEXBuilder
13+
from pex.pex_info import PexInfo
14+
15+
from pants.backend.python.python_requirement import PythonRequirement
16+
from pants.backend.python.tasks.pex_build_util import dump_requirements
17+
from pants.backend.python.tasks.wrapped_pex import WrappedPEX
18+
from pants.base.build_environment import get_pants_cachedir
19+
from pants.subsystem.subsystem import Subsystem
20+
from pants.util.dirutil import safe_concurrent_creation
21+
from pants.util.objects import datatype
22+
23+
24+
logger = logging.getLogger(__name__)
25+
26+
27+
class Conan(Subsystem):
28+
"""Pex binary for the conan package manager."""
29+
options_scope = 'conan'
30+
default_conan_requirements = (
31+
'conan==1.4.4',
32+
'PyJWT>=1.4.0, <2.0.0',
33+
'requests>=2.7.0, <3.0.0',
34+
'colorama>=0.3.3, <0.4.0',
35+
'PyYAML>=3.11, <3.13.0',
36+
'patch==1.16',
37+
'fasteners>=0.14.1',
38+
'six>=1.10.0',
39+
'node-semver==0.2.0',
40+
'distro>=1.0.2, <1.2.0',
41+
'pylint>=1.8.1, <1.9.0',
42+
'future==0.16.0',
43+
'pygments>=2.0, <3.0',
44+
'astroid>=1.6, <1.7',
45+
'deprecation>=2.0, <2.1'
46+
)
47+
48+
@classmethod
49+
def implementation_version(cls):
50+
return super(Conan, cls).implementation_version() + [('Conan', 0)]
51+
52+
@classmethod
53+
def register_options(cls, register):
54+
super(Conan, cls).register_options(register)
55+
register('--conan-requirements', type=list, default=cls.default_conan_requirements,
56+
advanced=True, help='The requirements used to build the conan client pex.')
57+
58+
class ConanBinary(datatype(['pex'])):
59+
"""A `conan` PEX binary."""
60+
pass
61+
62+
def bootstrap_conan(self):
63+
pex_info = PexInfo.default()
64+
pex_info.entry_point = 'conans.conan'
65+
conan_bootstrap_dir = os.path.join(get_pants_cachedir(), 'conan_support')
66+
conan_pex_path = os.path.join(conan_bootstrap_dir, 'conan_binary')
67+
interpreter = PythonInterpreter.get()
68+
if os.path.exists(conan_pex_path):
69+
conan_binary = WrappedPEX(PEX(conan_pex_path, interpreter))
70+
return self.ConanBinary(pex=conan_binary)
71+
else:
72+
with safe_concurrent_creation(conan_pex_path) as safe_path:
73+
builder = PEXBuilder(safe_path, interpreter, pex_info=pex_info)
74+
reqs = [PythonRequirement(req) for req in self.get_options().conan_requirements]
75+
dump_requirements(builder, interpreter, reqs, logger)
76+
builder.freeze()
77+
conan_binary = WrappedPEX(PEX(conan_pex_path, interpreter))
78+
return self.ConanBinary(pex=conan_binary)

src/python/pants/backend/native/subsystems/native_toolchain.py

+38-21
Original file line numberDiff line numberDiff line change
@@ -71,39 +71,44 @@ def select_linker(platform, native_toolchain):
7171
# 'darwin': lambda: Get(Linker, XCodeCLITools, native_toolchain._xcode_cli_tools),
7272
# 'linux': lambda: Get(Linker, Binutils, native_toolchain._binutils),
7373
# })
74+
#
75+
# NB: We need to link through a provided compiler's frontend, and we need to know where all the
76+
# compiler's libraries/etc are, so we set the executable name to the C++ compiler, which can find
77+
# its own set of C++-specific files for the linker if necessary. Using e.g. 'g++' as the linker
78+
# appears to produce byte-identical output when linking even C-only object files, and also
79+
# happens to work when C++ is used.
80+
# Currently, OSX links through the clang++ frontend, and Linux links through the g++ frontend.
7481
if platform.normalized_os_name == 'darwin':
7582
# TODO(#5663): turn this into LLVM when lld works.
7683
linker = yield Get(Linker, XCodeCLITools, native_toolchain._xcode_cli_tools)
84+
llvm_c_compiler = yield Get(LLVMCCompiler, NativeToolchain, native_toolchain)
85+
c_compiler = llvm_c_compiler.c_compiler
86+
llvm_cpp_compiler = yield Get(LLVMCppCompiler, NativeToolchain, native_toolchain)
87+
cpp_compiler = llvm_cpp_compiler.cpp_compiler
7788
else:
7889
linker = yield Get(Linker, Binutils, native_toolchain._binutils)
90+
gcc_c_compiler = yield Get(GCCCCompiler, NativeToolchain, native_toolchain)
91+
c_compiler = gcc_c_compiler.c_compiler
92+
gcc_cpp_compiler = yield Get(GCCCppCompiler, NativeToolchain, native_toolchain)
93+
cpp_compiler = gcc_cpp_compiler.cpp_compiler
7994

8095
libc_dirs = native_toolchain._libc_dev.get_libc_dirs(platform)
8196

82-
# NB: We need to link through a provided compiler's frontend, and we need to know where all the
83-
# compiler's libraries/etc are, so we set the executable name to the C++ compiler, which can find
84-
# its own set of C++-specific files for the linker if necessary. Using e.g. 'g++' as the linker
85-
# appears to produce byte-identical output when linking even C-only object files, and also
86-
# happens to work when C++ is used.
87-
gcc_c_compiler = yield Get(GCCCCompiler, NativeToolchain, native_toolchain)
88-
c_compiler = gcc_c_compiler.c_compiler
89-
gcc_cpp_compiler = yield Get(GCCCppCompiler, NativeToolchain, native_toolchain)
90-
cpp_compiler = gcc_cpp_compiler.cpp_compiler
91-
9297
# NB: If needing to create an environment for process invocation that could use either a compiler
9398
# or a linker (e.g. when we compile native code from `python_dist()`s), use the environment from
9499
# the linker object (in addition to any further customizations), which has the paths from the C
95100
# and C++ compilers baked in.
96101
# FIXME(#5951): we need a way to compose executables more hygienically.
97102
linker = Linker(
98103
path_entries=(
99-
c_compiler.path_entries +
100104
cpp_compiler.path_entries +
105+
c_compiler.path_entries +
101106
linker.path_entries),
102107
exe_filename=cpp_compiler.exe_filename,
103108
library_dirs=(
104109
libc_dirs +
105-
c_compiler.library_dirs +
106110
cpp_compiler.library_dirs +
111+
c_compiler.library_dirs +
107112
linker.library_dirs))
108113

109114
yield linker
@@ -147,7 +152,7 @@ def select_llvm_cpp_compiler(platform, native_toolchain):
147152
path_entries=(provided_clangpp.path_entries + xcode_clang.path_entries),
148153
exe_filename=provided_clangpp.exe_filename,
149154
library_dirs=(provided_clangpp.library_dirs + xcode_clang.library_dirs),
150-
include_dirs=(xcode_clang.include_dirs + provided_clangpp.include_dirs))
155+
include_dirs=(provided_clangpp.include_dirs + xcode_clang.include_dirs))
151156
final_llvm_cpp_compiler = LLVMCppCompiler(clang_with_xcode_paths)
152157
else:
153158
gcc_cpp_compiler = yield Get(GCCCppCompiler, GCC, native_toolchain._gcc)
@@ -231,16 +236,28 @@ def select_gcc_cpp_compiler(platform, native_toolchain):
231236
yield final_gcc_cpp_compiler
232237

233238

234-
@rule(CCompiler, [Select(NativeToolchain)])
235-
def select_c_compiler(native_toolchain):
236-
llvm_c_compiler = yield Get(LLVMCCompiler, NativeToolchain, native_toolchain)
237-
yield llvm_c_compiler.c_compiler
239+
@rule(CCompiler, [Select(NativeToolchain), Select(Platform)])
240+
def select_c_compiler(native_toolchain, platform):
241+
if platform.normalized_os_name == 'darwin':
242+
llvm_c_compiler = yield Get(LLVMCCompiler, NativeToolchain, native_toolchain)
243+
c_compiler = llvm_c_compiler.c_compiler
244+
else:
245+
gcc_c_compiler = yield Get(GCCCCompiler, NativeToolchain, native_toolchain)
246+
c_compiler = gcc_c_compiler.c_compiler
247+
248+
yield c_compiler
249+
238250

251+
@rule(CppCompiler, [Select(NativeToolchain), Select(Platform)])
252+
def select_cpp_compiler(native_toolchain, platform):
253+
if platform.normalized_os_name == 'darwin':
254+
llvm_cpp_compiler = yield Get(LLVMCppCompiler, NativeToolchain, native_toolchain)
255+
cpp_compiler = llvm_cpp_compiler.cpp_compiler
256+
else:
257+
gcc_cpp_compiler = yield Get(GCCCppCompiler, NativeToolchain, native_toolchain)
258+
cpp_compiler = gcc_cpp_compiler.cpp_compiler
239259

240-
@rule(CppCompiler, [Select(NativeToolchain)])
241-
def select_cpp_compiler(native_toolchain):
242-
llvm_cpp_compiler = yield Get(LLVMCppCompiler, NativeToolchain, native_toolchain)
243-
yield llvm_cpp_compiler.cpp_compiler
260+
yield cpp_compiler
244261

245262

246263
def create_native_toolchain_rules():
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# coding=utf-8
2+
# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
3+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
4+
5+
from __future__ import absolute_import, division, print_function, unicode_literals
6+
7+
import re
8+
9+
from pants.base.payload import Payload
10+
from pants.base.payload_field import PrimitiveField
11+
from pants.base.validation import assert_list
12+
from pants.build_graph.target import Target
13+
14+
15+
class ExternalNativeLibrary(Target):
16+
"""A set of Conan package strings to be passed to the Conan package manager."""
17+
18+
@classmethod
19+
def alias(cls):
20+
return 'external_native_library'
21+
22+
def __init__(self, payload=None, packages=None, **kwargs):
23+
"""
24+
:param packages: a list of Conan-style package strings
25+
26+
Example:
27+
lzo/2.10@twitter/stable
28+
"""
29+
payload = payload or Payload()
30+
31+
assert_list(packages, key_arg='packages')
32+
payload.add_fields({
33+
'packages': PrimitiveField(packages),
34+
})
35+
super(ExternalNativeLibrary, self).__init__(payload=payload, **kwargs)
36+
37+
@property
38+
def packages(self):
39+
return self.payload.packages
40+
41+
@property
42+
def lib_names(self):
43+
return [re.match(r'^([^\/]+)\/', pkg_name).group(1) for pkg_name in self.payload.packages]

src/python/pants/backend/native/tasks/cpp_compile.py

+11
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ def get_compile_settings(self):
4141
def get_compiler(self):
4242
return self._request_single(CppCompiler, self._toolchain)
4343

44+
def _make_compile_argv(self, compile_request):
45+
# FIXME: this is a temporary fix, do not do any of this kind of introspection.
46+
# https://github.com/pantsbuild/pants/issues/5951
47+
prev_argv = super(CppCompile, self)._make_compile_argv(compile_request)
48+
49+
if compile_request.compiler.exe_filename == 'clang++':
50+
new_argv = [prev_argv[0], '-nobuiltininc', '-nostdinc++'] + prev_argv[1:]
51+
else:
52+
new_argv = prev_argv
53+
return new_argv
54+
4455
# FIXME(#5951): don't have any command-line args in the task or in the subsystem -- rather,
4556
# subsystem options should be used to populate an `Executable` which produces its own arguments.
4657
def extra_compile_args(self):

0 commit comments

Comments
 (0)