Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC][feature] find_libraries function #15866

Draft
wants to merge 8 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion conan/tools/files/__init__.py
@@ -1,6 +1,6 @@
from conan.tools.files.files import load, save, mkdir, rmdir, rm, ftp_download, download, get, \
rename, chdir, unzip, replace_in_file, collect_libs, check_md5, check_sha1, check_sha256, \
move_folder_contents
move_folder_contents, fetch_libraries

from conan.tools.files.patches import patch, apply_conandata_patches, export_conandata_patches
from conan.tools.files.cpp_package import CppPackage
Expand Down
109 changes: 106 additions & 3 deletions conan/tools/files/files.py
Expand Up @@ -9,16 +9,15 @@
import sys
from contextlib import contextmanager
from fnmatch import fnmatch

import six
from urllib.parse import urlparse
from urllib.request import url2pathname

import six
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No six import should be necessary anymore! I thought we had gotten rid of this 😅


from conan.tools import CONAN_TOOLCHAIN_ARGS_FILE, CONAN_TOOLCHAIN_ARGS_SECTION
from conans.client.downloaders.download import run_downloader
from conans.errors import ConanException
from conans.util.files import rmdir as _internal_rmdir
from conans.util.runners import check_output_runner

if six.PY3: # Remove this IF in develop2
from shutil import which
Expand Down Expand Up @@ -543,6 +542,110 @@ def collect_libs(conanfile, folder=None):
return result


def fetch_libraries(conanfile, cpp_info=None, cpp_info_libs=None,
extra_folders=None, win_interface_error=False) -> list[tuple[str, str, str, str]]:
"""
Get all the static/shared library paths, or those associated with the ``cpp_info.libs`` or the given
by ``cpp_info_libs`` parameter (useful to search libraries associated to a library name/es).
If Windows, it analyzes if the DLLs are present and raises an exception if the interface library
is not present (if ``win_interface_error == True``).

:param conanfile: normally a ``<ConanFileInterface obj>``.
:param cpp_info: Likely ``<CppInfo obj>`` of the component.
:param cpp_info_libs: list of associated libraries to search. If an empty list is passed,
the function will look for all the libraries in the folders.
Otherwise, if ``None``, it'll use the ``cpp_info.libs``.
:param extra_folders: list of relative folders (relative to `package_folder`) to search more libraries.
:param win_interface_error: if ``raise_error == True``, it'll raise an exception if DLL lib does not have
an associated *.lib interface library.
:return: list of tuples per static/shared library ->
[(lib_name, library_path, interface_library_path, symlink_library_path)]
Note: ``library_path`` could be both static and shared ones in case of UNIX systems.
Windows would have:
* shared: library_path as DLL, and interface_library_path as LIB
* static: library_path as LIB, and interface_library_path as ""
"""
def _save_lib_path(lib_name, lib_path_):
"""Add each lib with its full library path"""
formatted_path = lib_path_.replace("\\", "/")
_, ext_ = os.path.splitext(formatted_path)
# In case of symlinks, let's save where they point to (all of them)
if os.path.islink(formatted_path) and lib_name not in symlink_paths:
# Important! os.path.realpath returns the final path of the symlink even if it points
# to another symlink, i.e., libmylib.dylib -> libmylib.1.dylib -> libmylib.1.0.0.dylib
# then os.path.realpath("libmylib.dylib") == "libmylib.1.0.0.dylib"
# Better to use os.readlink as it returns the path which the symbolic link points to.
symlink_paths[lib_name] = os.readlink(formatted_path)
lib_paths[lib_name] = formatted_path
return
# Likely Windows interface library (or static one)
elif ext_ == ".lib" and lib_name not in interface_lib_paths:
interface_lib_paths[lib_name] = formatted_path
return # putting it apart from the rest
if lib_name not in lib_paths:
# Save the library
lib_paths[lib_name] = formatted_path

cpp_info = cpp_info or conanfile.cpp_info
libdirs = cpp_info.libdirs
# Just want to get shared libraries (likely DLLs)
bindirs = cpp_info.bindirs
# Composing absolute folders if extra folders were passed
extra_paths = [os.path.join(conanfile.package_folder, path) for path in extra_folders or []]
folders = set(libdirs + bindirs + (extra_paths or []))
# Gather all the libraries based on the cpp_info.libs if cpp_info_libs arg is None
# Pass libs=[] if you want to collect all the libraries regardless the matches with the name
associated_libs = cpp_info.libs[:] if cpp_info_libs is None else cpp_info_libs
lib_paths = {}
interface_lib_paths = {}
symlink_paths = {}
for libdir in sorted(folders): # deterministic
if not os.path.exists(libdir):
continue
files = os.listdir(libdir)
for f in files:
full_path = os.path.join(libdir, f)
if not os.path.isfile(full_path): # Make sure that directories are excluded
continue

name, ext = os.path.splitext(f)
# Users may not name their libraries in a conventional way. For example, directly
# use the basename of the lib file as lib name, e.g., cpp_info.libs = ["liblib1.a"]
# Issue related: https://github.com/conan-io/conan/issues/11331
if ext and f in associated_libs: # let's ensure that it has any extension
_save_lib_path(f, full_path)
continue
if name not in associated_libs and name.startswith("lib"):
name = name[3:] # libpkg -> pkg
if ext in (".so", ".lib", ".a", ".dylib", ".bc", ".dll"):
if associated_libs:
# Alternative name: in some cases the name could be pkg.if instead of pkg
base_name = name.split(".", maxsplit=1)[0]
if base_name in associated_libs:
_save_lib_path(base_name, full_path) # passing pkg instead of pkg.if
else: # we want to save all the libs
_save_lib_path(name, full_path)

libraries = []
for lib, lib_path in lib_paths.items():
interface_lib_path = ""
symlink_path = symlink_paths.get(lib, "")
if lib_path.endswith(".dll"):
if lib not in interface_lib_paths:
msg = f"Windows needs a .lib for link-time and .dll for runtime. Only found {lib_path}"
if win_interface_error:
raise ConanException(msg)
else:
conanfile.output.warn(msg)
interface_lib_path = interface_lib_paths.pop(lib, "")
libraries.append((lib, lib_path, interface_lib_path, symlink_path))
if interface_lib_paths: # Rest of static .lib Windows libraries
libraries.extend([(lib_name, lib_path, "", "")
for lib_name, lib_path in interface_lib_paths.items()])
libraries.sort()
return libraries


def move_folder_contents(conanfile, src_folder, dst_folder):
""" replaces the dst_folder contents with the contents of the src_folder, which can be a
child folder of dst_folder. This is used in the SCM monorepo flow, when it is necessary
Expand Down
83 changes: 2 additions & 81 deletions conan/tools/google/bazeldeps.py
Expand Up @@ -7,6 +7,7 @@

from conan.errors import ConanException
from conan.tools._check_build_profile import check_using_build_profile
from conan.tools.files import fetch_libraries
from conans.util.files import save

_BazelTargetInfo = namedtuple("DepInfo", ['repository_name', 'name', 'requires', 'cpp_info'])
Expand Down Expand Up @@ -71,86 +72,6 @@ def _get_requirements(conanfile, build_context_activated):
yield require, dep


def _get_libs(dep, cpp_info=None) -> list:
"""
Get the static/shared library paths

:param dep: normally a <ConanFileInterface obj>
:param cpp_info: <CppInfo obj> of the component.
:return: list of tuples per static/shared library ->
[(lib_name, is_shared, library_path, interface_library_path)]
Note: ``library_path`` could be both static and shared ones in case of UNIX systems.
Windows would have:
* shared: library_path as DLL, and interface_library_path as LIB
* static: library_path as LIB, and interface_library_path as None
"""
def _is_shared():
"""
Checking traits and shared option
"""
default_value = dep.options.get_safe("shared") if dep.options else False
# Conan 2.x
# return {"shared-library": True,
# "static-library": False}.get(str(dep.package_type), default_value)
return default_value

def _save_lib_path(lib_, lib_path_):
"""Add each lib with its full library path"""
formatted_path = lib_path_.replace("\\", "/")
_, ext_ = os.path.splitext(formatted_path)
if is_shared and ext_ == ".lib": # Windows interface library
interface_lib_paths[lib_] = formatted_path
else:
lib_paths[lib_] = formatted_path

cpp_info = cpp_info or dep.cpp_info
is_shared = _is_shared()
libdirs = cpp_info.libdirs
bindirs = cpp_info.bindirs if is_shared else [] # just want to get shared libraries
libs = cpp_info.libs[:] # copying the values
lib_paths = {}
interface_lib_paths = {}
for libdir in set(libdirs + bindirs):
if not os.path.exists(libdir):
continue
files = os.listdir(libdir)
for f in files:
full_path = os.path.join(libdir, f)
if not os.path.isfile(full_path): # Make sure that directories are excluded
continue
name, ext = os.path.splitext(f)
# Users may not name their libraries in a conventional way. For example, directly
# use the basename of the lib file as lib name, e.g., cpp_info.libs = ["liblib1.a"]
# Issue related: https://github.com/conan-io/conan/issues/11331
if ext and f in libs: # let's ensure that it has any extension
_save_lib_path(f, full_path)
continue
if name not in libs and name.startswith("lib"):
name = name[3:] # libpkg -> pkg
# FIXME: Should it read a conf variable to know unexpected extensions?
if (is_shared and ext in (".so", ".dylib", ".lib", ".dll")) or \
(not is_shared and ext in (".a", ".lib")):
if name in libs:
_save_lib_path(name, full_path)
continue
else: # last chance: some cases the name could be pkg.if instead of pkg
name = name.split(".", maxsplit=1)[0]
if name in libs:
_save_lib_path(name, full_path)

libraries = []
for lib, lib_path in lib_paths.items():
interface_lib_path = None
if lib_path.endswith(".dll"):
if lib not in interface_lib_paths:
raise ConanException(f"Windows needs a .lib for link-time and .dll for runtime."
f" Only found {lib_path}")
interface_lib_path = interface_lib_paths.pop(lib)
libraries.append((lib, is_shared, lib_path, interface_lib_path))
# TODO: Would we want to manage the cases where DLLs are provided by the system?
return libraries


def _get_headers(cpp_info, package_folder_path):
return ['"{}/**"'.format(_relativize_path(path, package_folder_path))
for path in cpp_info.includedirs]
Expand Down Expand Up @@ -417,7 +338,7 @@ def fill_info(info):
# os_build = self._dep.settings_build.get_safe("os")
os_build = self._dep.settings.get_safe("os")
linkopts = _get_linkopts(cpp_info, os_build)
libs = _get_libs(self._dep, cpp_info)
libs = fetch_libraries(self._dep, cpp_info)
libs_info = []
bindirs = [_relativize_path(bindir, package_folder_path)
for bindir in cpp_info.bindirs]
Expand Down