diff --git a/conan/tools/b2/__init__.py b/conan/tools/b2/__init__.py new file mode 100644 index 00000000000..92de533fd77 --- /dev/null +++ b/conan/tools/b2/__init__.py @@ -0,0 +1 @@ +from conan.tools.b2.b2deps import B2Deps diff --git a/conan/tools/b2/b2deps.py b/conan/tools/b2/b2deps.py new file mode 100644 index 00000000000..518d9f8bb63 --- /dev/null +++ b/conan/tools/b2/b2deps.py @@ -0,0 +1,290 @@ +from conan.internal import check_duplicated_generator +from conan.tools.b2.util import * +from conans.errors import ConanException +from conans.util.files import save, chdir +from conans.paths import get_conan_user_home +from hashlib import md5 + + +class B2Deps(object): + """ + B2Deps generates files that are automatically loaded by the B2 build system. + The files define localized subprojects and targets for dependencies. + """ + + def __init__(self, conanfile): + self._conanfile = conanfile + self._conanhome = get_conan_user_home() + + def generate(self): + """ + This method will save the generated files to the conanfile.source_folder + """ + source_folder = self._conanfile.source_folder + self._conanfile.output.highlight(f"Writing B2Deps to {source_folder}") + with chdir(source_folder): + check_duplicated_generator(self, self._conanfile) + generator_files = self.content + for generator_file, content in generator_files.items(): + self._conanfile.output.info(f"Saved B2Deps file {generator_file}") + save(generator_file, content) + + @property + def content(self): + """ + Generates two content files: conanbuildinfo.jam and + conanbuildinfo-ID.jam. The former defines common package definition + function and include the latter conanbuildinfo-ID.jam files. The + conanbuildinfo-ID.jam files define sub-projects and targets for each + settings variation. The generated files have stable names and content. + Hence they can be added to source control. + """ + self._content = {} + self._content_conanbuildinfo_jam() + self._content_conanbuildinfo_variation_jam() + for ck in self._content.keys(): + self._content[ck] = self._conanbuildinfo_header_text+"\n"+self._content[ck] + return self._content + + def _content_conanbuildinfo_jam(self): + # Generate the common conanbuildinfo.jam which does four things: + # + # Defines common utility functions to make the rest of the code short + # and includes the conanbuildinfo-*.jam sub-files. + cbi = [self._conanbuildinfo_common_text] + # The combined text. + self._content['conanbuildinfo.jam'] = "\n".join(cbi) + + def _content_conanbuildinfo_variation_jam(self): + # Generate the current build variation conanbuildinfo-/variation/.jam. + for require, dependency in self._conanfile.dependencies.items(): + # Only generate defs for direct dependencies. + if not require.direct: + continue + # The base name of the dependency. + dep_name = dependency.ref.name + # B2 equivalent of the dependency name. We keep all names lower case. + dep_name_b2 = dep_name.lower() + # The dependency cpp_info. We need to consider that there's a + # "_depname" component. Such components are a kludge to appease + # cmake generators and will eventually go away. This special + # component holds the real root definitions of the dependency. + dep_cpp_info = dependency.cpp_info + if '_'+dep_name in dep_cpp_info.components: + dep_cpp_info = dep_cpp_info.components['_'+dep_name] + # The settings and options the dependency requires, i.e. finds relevant. + variant_settings = self._conanfile.settings + variant_options = dependency.options + # The variant specific file to add this dependency to. + dep_variant_jam = B2Deps._conanbuildinfo_variation_jam( + dep_name_b2, variant_settings, variant_options) + if not dep_variant_jam in self._content: + self._content[dep_variant_jam] = "" + # Declare/define the local project for the dependency. + cbiv = [ + '#|', + f'{dependency.pref}', + '[settings]', + variant_settings.dumps(), + '[options]', + variant_options.dumps(), + '|#', + f'pkg-project {dep_name_b2} ;'] + # Declare any system libs that we refer to (in usage requirements). + system_libs = set(dependency.cpp_info.system_libs) + for name, component in dependency.cpp_info.get_sorted_components().items(): + system_libs |= set(component.system_libs) + cbiv += self._content_conanbuildinfo_variation_declare_syslibs( + dep_name_b2, system_libs, settings=variant_settings, options=variant_options) + # Declare any package libs for usage requirements. The first one is + # the main/global dependency. + cbiv += self._content_conanbuildinfo_variation_declare_libs( + dep_name_b2, dep_cpp_info, settings=variant_settings, options=variant_options) + # Followed by any components of the dependency. But skipping the + # special _depname component. As that is already declare as the + # main/global lib. + for name, component in dependency.cpp_info.get_sorted_components().items(): + if name.lower() == '_'+dep_name_b2: + continue + cbiv += self._content_conanbuildinfo_variation_declare_libs( + dep_name_b2, component, settings=variant_settings, options=variant_options) + # Declare the main target of the dependency. This is an alias that + # refers to all the previous targets and adds all the defines, + # flags, etc for consumers. + cbiv += self._content_conanbuildinfo_variation_declare_target( + dep_name_b2, dep_name_b2, + dep_cpp_info, + settings=variant_settings, options=variant_options) + # Similarly declare the component targets, if any. + for name, component in dependency.cpp_info.get_sorted_components().items(): + # Again, always skipping the kludge component as it's already + # defined. + if "_"+dep_name_b2 == name.lower(): + continue + cbiv += self._content_conanbuildinfo_variation_declare_target( + dep_name_b2, name.lower(), component, settings=variant_settings, options=variant_options) + # Add the combined text. + self._content[dep_variant_jam] += "\n".join(cbiv)+"\n" + + def _content_conanbuildinfo_variation_declare_libs(self, name, cpp_info, settings=None, options=None): + name = name.lower() + cbi_libs = [] + variation = ' '.join(b2_features(b2_variation(settings, options))) + for lib in cpp_info.libs: + search = ' '.join( + [f'"{b2_path(d.replace(self._conanhome, "$(CONAN_HOME)"))}"' for d in cpp_info.libdirs+cpp_info.bindirs]) + # The lib targets are prefixed with "lib." to distinguish them + # from dependency main targets as it's often the case that the + # dependency has the same name as the library consumers link to. + cbi_libs += [ + f'pkg-lib {name}//lib.{lib} : : {lib}', + f' {variation}', + f' {search} ;'] + return cbi_libs + + def _content_conanbuildinfo_variation_declare_syslibs(self, name, systemlibs, settings=None, options=None): + name = name.lower() + cbi_libs = [] + variation = ' '.join(b2_features(b2_variation(settings, options))) + for lib in systemlibs: + # Although system libs won't collide in the names. We still prefix + # the target names with "lib." for consistency and easier reference + # in the main targets. + cbi_libs += [ + f'pkg-lib {name}//lib.{lib} : : {lib}', + f' {variation} ;'] + return cbi_libs + + def _content_conanbuildinfo_variation_declare_target(self, name, target, cpp_info, settings=None, options=None): + cbi_target = [] + # Target, no sources. The empty target is to catch incompatible build + # requirements matches by falling back to an unbuildable result. + cbi_target += [ + f'pkg-alias {name}//{target} : : no ;', + f'pkg-alias {name}//{target} : :'] + # Requirements: + cbi_target += [ + f' {" ".join(b2_features(b2_variation(settings, options)))}'] + cbi_target += [ + f' lib.{l}' for l in cpp_info.libs+cpp_info.system_libs] + # No default-build: + cbi_target += [" : :"] + # Usage-requirements: + cbi_target += [ + f' "{b2_path(d.replace(self._conanhome, "$(CONAN_HOME)"))}"' for d in cpp_info.includedirs] + cbi_target += [f' "{d}"' for d in cpp_info.defines] + cbi_target += [f' "{f}"' for f in cpp_info.cflags] + cbi_target += [f' "{f}"' for f in cpp_info.cxxflags] + cbi_target += [ + f' SHARED_LIB:"{f}"' for f in cpp_info.sharedlinkflags] + cbi_target += [f' EXE:"{f}"' for f in cpp_info.exelinkflags] + cbi_target += [" ;"] + return cbi_target + + @staticmethod + def _conanbuildinfo_variation_jam(name, settings, options=None): + return 'conanbuildinfo-{}-{}.jam'.format( + name, b2_variation_key(settings, options)) + + _conanbuildinfo_header_text = """\ +#| + B2 definitions for Conan packages. This is a generated file. + Edit the corresponding conanfile.txt/py instead. +|# +""" + + _conanbuildinfo_common_text = """\ +import path ; +import project ; +import modules ; +import feature ; +import os ; + +rule pkg-project ( id ) +{ + local id-mod = [ project.find $(id:L) : . ] ; + if ! $(id-mod) + { + local parent-prj = [ project.current ] ; + local parent-mod = [ $(parent-prj).project-module ] ; + local id-location = [ path.join + [ project.attribute $(parent-mod) location ] + $(id:L) ] ; + id-mod = [ project.load $(id-location) : synthesize ] ; + project.push-current [ project.current ] ; + project.initialize $(id-mod) : $(id-location) ; + project.pop-current ; + project.inherit-attributes $(id-mod) : $(parent-mod) ; + local attributes = [ project.attributes $(id-mod) ] ; + $(attributes).set parent-module : $(parent-mod) : exact ; + if [ project.is-jamroot-module $(parent-mod) ] + { + use-project /$(id:L) : $(id:L) ; + } + } + return $(id-mod) ; +} + +rule pkg-target ( target : sources * : requirements * : default-build * : usage-requirements * ) +{ + target = [ MATCH "(.*)//(.*)" : $(target) ] ; + local id-mod = [ pkg-project $(target[1]) ] ; + project.push-current [ project.target $(id-mod) ] ; + local bt = [ BACKTRACE 1 ] ; + local rulename = [ MATCH "pkg-(.*)" : $(bt[4]) ] ; + modules.call-in $(id-mod) : + $(rulename) $(target[2]) : $(sources) : $(requirements) : $(default-build) + : $(usage-requirements) ; + project.pop-current ; +} + +IMPORT $(__name__) : pkg-target : $(__name__) : pkg-alias ; +IMPORT $(__name__) : pkg-target : $(__name__) : pkg-lib ; + +rule conan-home ( ) +{ + local conan_home = [ os.environ CONAN_HOME ] ; + if ! $(conan_home) + { + local conanrc = [ path.glob-in-parents [ path.join [ path.pwd ] "_" ] : ".conanrc" ] ; + if $(conanrc) + { + local conanrc_file = [ FILE_OPEN [ path.native $(conanrc) ] : t ] ; + conan_home = [ MATCH "^conan_home=(.*) +" ^conan_home=(.*) : $(conanrc_file) ] ; + conan_home = [ path.make $(conan_home[1]) ] ; + if [ MATCH ^(~/) : $(conan_home) ] + { + local home = [ os.home-directories ] ; + conan_home = [ path.join [ path.make $(home[1]) ] [ MATCH "^~/(.*)" : $(conan_home) ] ] ; + } + else if ! [ path.is-rooted $(conan_home) ] + { + conan_home = [ path.root $(conan_home) $(conanrc:D) ] ; + } + } + } + if ! $(conan_home) + { + local home = [ os.home-directories ] ; + conan_home = [ path.join [ path.make $(home[1]) ] .conan2 ] ; + } + return $(conan_home) ; +} + +path-constant CONAN_HOME : [ conan-home ] ; + +if ! ( relwithdebinfo in [ feature.values variant ] ) +{ + variant relwithdebinfo : : speed on full off ; +} +if ! ( minsizerel in [ feature.values variant ] ) +{ + variant minsizerel : : space off full off ; +} + +for local __cbi__ in [ GLOB $(__file__:D) : conanbuildinfo-*.jam ] +{ + include $(__cbi__) ; +} +""" diff --git a/conan/tools/b2/util.py b/conan/tools/b2/util.py new file mode 100644 index 00000000000..8cc440eeffb --- /dev/null +++ b/conan/tools/b2/util.py @@ -0,0 +1,262 @@ +from conans.errors import ConanException +from hashlib import md5 +from base64 import b32encode + +__all__ = [ + 'b2_address_model', + 'b2_architecture', + 'b2_cxxstd_dialect', + 'b2_cxxstd', + 'b2_features', + 'b2_instruction_set', + 'b2_link', + 'b2_os', + 'b2_path', + 'b2_runtime_debugging', + 'b2_runtime_link', + 'b2_threadapi', + 'b2_toolset', + 'b2_variant', + 'b2_variation_id', + 'b2_variation_key', + 'b2_variation', +] + + +def b2_architecture(conan_arch): + if conan_arch is None: + return None + elif conan_arch.startswith('x86'): + return 'x86' + elif conan_arch.startswith('ppc'): + return 'power' + elif conan_arch.startswith('arm'): + return 'arm' + elif conan_arch.startswith('sparc'): + return 'sparc' + elif conan_arch.startswith('mips'): + return conan_arch + else: + return None + + +def b2_instruction_set(conan_arch): + if conan_arch is None: + return None + elif conan_arch.startswith('armv6'): + return 'armv6' + elif conan_arch.startswith('armv7'): + return 'armv7' + elif conan_arch.startswith('armv7s'): + return 'armv7s' + elif conan_arch.startswith('sparcv9'): + return 'v9' + else: + return None + + +def b2_address_model(conan_arch): + if conan_arch is None: + return None + elif '32' in conan_arch: + return '32' + elif '64' in conan_arch: + return '64' + elif conan_arch in ['x86', 'mips']: + return '32' + elif conan_arch in ['sparcv9']: + return '64' + elif conan_arch.startswith('arm'): + if conan_arch.startswith('armv8'): + return '64' + else: + return '32' + elif conan_arch.startswith('sparc'): + return '32' + else: + return None + + +def b2_os(conan_os, conan_os_subsystem=None): + if conan_os is None: + return None + conan_os = conan_os.lower() + if conan_os.startswith('windows'): + return 'windows' + elif conan_os in ['macos', 'ios', 'watchos', 'tvos']: + return 'darwin' + elif conan_os == 'subos': + return 'solaris' + elif conan_os in ['arduino']: + return 'linux' + elif conan_os == 'windows' and conan_os_subsystem == 'cygwin': + return 'cygwin' + else: + return conan_os + + +def b2_variant(conan_build_type): + if conan_build_type is None: + return None + return conan_build_type.lower() + + +def b2_cxxstd(conan_cppstd): + if conan_cppstd is None: + return None + return conan_cppstd.replace('gnu', '') if conan_cppstd else None + + +def b2_cxxstd_dialect(conan_cppstd): + if conan_cppstd is None: + return None + if conan_cppstd and 'gnu' in conan_cppstd: + return 'gnu' + else: + return None + + +def b2_toolset(conan_compiler, conan_compiler_version): + if conan_compiler is None: + return None + toolset = conan_compiler.lower() + if 'clang' in conan_compiler: + toolset = 'clang' + elif 'sun-cc' == conan_compiler: + toolset = 'sun' + elif 'Visual Studio' == conan_compiler: + toolset = 'msvc' + elif 'intel' in conan_compiler: + toolset = 'intel' + if not conan_compiler_version: + return toolset + version = conan_compiler_version + if toolset == 'msvc': + if conan_compiler_version == '15': + version = '14.1' + else: + version = conan_compiler_version + '.0' + return toolset + '-' + version + + +def b2_path(path): + """ + Adjust a regular path to the form b2 can use in source code. + """ + return path.replace('\\', '/') + + +def b2_features(features): + """ + Generate a b2 requirements list, i.e. value list, from the given + 'features' dict. + """ + result = [] + for k, v in sorted(features.items()): + if v: + result += ['<%s>%s' % (k, v)] + return result + + +def b2_threadapi(conan_compiler_threads): + if conan_compiler_threads is None: + return None + conan_compiler_threads = conan_compiler_threads.lower() + if conan_compiler_threads == 'posix': + return 'pthread' + + +def b2_runtime_link(conan_compiler_runtime): + if conan_compiler_runtime is None: + return None + if conan_compiler_runtime in ['static', 'MT', 'MTd']: + return 'static' + return 'shared' + + +def b2_runtime_debugging(conan_compiler_runtime): + if conan_compiler_runtime is None: + return None + if conan_compiler_runtime in ['Debug', 'MTd', 'MDd']: + return 'on' + return 'off' + + +def b2_link(conan_options_shared): + if conan_options_shared is None: + return None + if conan_options_shared: + return 'shared' + return 'static' + + +def _get_setting(settings, name, default=None, optional=True): + result = settings.get_safe(name, default) if settings else None + if not result and not optional: + raise ConanException( + "Need 'settings.{}', but it is not defined.".format(name)) + return result + + +def _get_option(options, name, default=None, optional=True): + result = options.get_safe(name, default) if options else None + if not result and not optional: + raise ConanException( + "Need 'options.{}', but it is not defined.".format(name)) + return result + + +def b2_variation(settings, options=None): + """ + Returns a map of b2 features & values as translated from conan settings + and options that can affect the link compatibility of libraries. + """ + _b2_variation_v = { + 'toolset': b2_toolset( + _get_setting(settings, "compiler"), + _get_setting(settings, "compiler.version")), + 'architecture': b2_architecture( + _get_setting(settings, "arch")), + 'instruction-set': b2_instruction_set( + _get_setting(settings, "arch")), + 'address-model': b2_address_model( + _get_setting(settings, "arch")), + 'target-os': b2_os( + _get_setting(settings, "os"), + _get_setting(settings, "os.subsystem")), + 'variant': b2_variant( + _get_setting(settings, "build_type")), + 'cxxstd': b2_cxxstd( + _get_setting(settings, "cppstd")), + 'cxxstd:dialect': b2_cxxstd_dialect( + _get_setting(settings, "cppstd")), + 'threadapi': b2_threadapi( + _get_setting(settings, 'compiler.threads')), + 'runtime-link': b2_runtime_link( + _get_setting(settings, 'compiler.runtime')), + 'runtime-debugging': b2_runtime_debugging( + _get_setting(settings, 'compiler.runtime_type')), + 'link': b2_link( + _get_option(options, 'shared')), + } + return _b2_variation_v + + +def b2_variation_id(settings, options=None): + """ + A compact single comma separated list of the variation are included in + sorted by feature name order. + """ + vid = [] + _b2_variation = b2_variation(settings, options) + for k in sorted(_b2_variation.keys()): + if _b2_variation[k] is not None: + vid += [k+'='+_b2_variation[k]] + return ",".join(vid) + + +def b2_variation_key(settings, options=None): + """ + A hashed key of the variation to use a UID for the variation. + """ + return b32encode(md5(b2_variation_id(settings, options).encode('utf-8')).digest()).decode('utf-8').lower().replace('=', '') diff --git a/conans/client/generators/__init__.py b/conans/client/generators/__init__.py index ad9f8b88771..bdec9ccfd51 100644 --- a/conans/client/generators/__init__.py +++ b/conans/client/generators/__init__.py @@ -21,6 +21,7 @@ "IntelCC": "conan.tools.intel", "XcodeDeps": "conan.tools.apple", "XcodeToolchain": "conan.tools.apple", "PremakeDeps": "conan.tools.premake", + "B2Deps": "conan.tools.b2", } diff --git a/conans/test/unittests/tools/b2/__init__.py b/conans/test/unittests/tools/b2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/conans/test/unittests/tools/b2/test_b2deps.py b/conans/test/unittests/tools/b2/test_b2deps.py new file mode 100644 index 00000000000..d23b0cbe3e8 --- /dev/null +++ b/conans/test/unittests/tools/b2/test_b2deps.py @@ -0,0 +1,57 @@ +import mock +from mock import Mock + +from conan.tools.b2 import B2Deps +from conan import ConanFile +from conans.model.build_info import CppInfo +from conans.model.conanfile_interface import ConanFileInterface +from conans.model.dependencies import ConanFileDependencies +from conans.model.recipe_ref import RecipeReference +from conans.model.requires import Requirement +from conans.model.settings import Settings + + +def test_cpp_info_name_b2deps(): + conanfile = ConanFile() + conanfile._conan_node = Mock() + conanfile._conan_node.context = "host" + conanfile.settings = "os", "compiler", "build_type", "arch" + conanfile.settings = Settings({ + "os": ["Windows"], + "compiler": ["gcc"], + "build_type": ["Release"], + "arch": ["x86"]}) + conanfile.settings.build_type = "Release" + conanfile.settings.arch = "x86" + + cpp_info = CppInfo(set_defaults=True) + + conanfile_dep = ConanFile(None) + conanfile_dep.cpp_info = cpp_info + conanfile_dep._conan_node = Mock() + conanfile_dep._conan_node.ref = RecipeReference.loads("OriginalDepName/1.0") + conanfile_dep._conan_node.context = "host" + conanfile_dep.settings = conanfile.settings + conanfile_dep.folders.set_base_package("/path/to/folder_dep") + # necessary, as the interface doesn't do it now automatically + conanfile_dep.cpp_info.set_relative_base_folder("/path/to/folder_dep") + + # FIXME: This will run infinite loop if conanfile.dependencies.host.topological_sort. + # Move to integration test + with mock.patch('conan.ConanFile.dependencies', new_callable=mock.PropertyMock) as mock_deps: + req = Requirement(RecipeReference.loads("OriginalDepName/1.0")) + mock_deps.return_value = ConanFileDependencies({ + req: ConanFileInterface(conanfile_dep)}) + + b2deps = B2Deps(conanfile) + files = b2deps.content + for k in sorted(files.keys()): + print("\n\n{}:\n---\n{}\n---".format(k, files[k])) + variation_filename = B2Deps._conanbuildinfo_variation_jam("originaldepname", conanfile.settings) + assert "conanbuildinfo.jam" in files + assert variation_filename in files + variation_file = files[variation_filename] + assert 'pkg-project originaldepname ;' in variation_file + assert 'pkg-alias originaldepname//originaldepname' in variation_file + assert 'x86' in variation_file + assert 'release' in variation_file