Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/markdown/Pkgconfig-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ keyword arguments.
*Since 1.9.0* internal dependencies are supported if `pkgconfig.generate()`
was used on the underlying library.
- `requires_private` the same as `requires` but for the `Requires.private` field
- `requires_shared` (*Since 1.11.0*) a list of strings or dependency objects
to add to the `Requires.private` field for shared libraries. When specified,
overrides the automatic `Requires.private` entries that would normally be
generated from the shared library's dependencies; this can be used to
reduce unnecessary build dependencies when the library's headers do not
expose its dependency's headers. Has no effect on static libraries.
- `cflags_private` (*Since 1.11.0*) compiler flags added when linking with a static
library. Note: currently, the FreeDesktop.org pkg-config implementation does
[not support Cflags.private](https://gitlab.freedesktop.org/pkg-config/pkg-config/-/issues/38)
Expand Down
7 changes: 7 additions & 0 deletions docs/markdown/snippets/pkgconfig-requires-cflags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## New `requires_shared` keyword argument for `pkgconfig.generate()`

The new `requires_shared` keyword argument to `pkgconfig.generate()` can
be used to override the automatically-generated `Requires.private` entries
for shared libraries. This is useful when a shared library's headers do
not expose its dependency's headers, avoiding unnecessary build dependencies
for consumers of the `.pc` file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## `Requires.private` includes dependencies of shared libraries

When a `.pc` file is created for a `shared_library()`, its `Requires.private`
line now always includes the dependencies of the shared library. These are used
when `pkg-config` or `pkgconf` are invoked with `--cflags`. Previously,
the line was included for shared libraries created with `library()` (even
if the `default_library` option was set to `shared`), whereas now
`library()` and `shared_library()` behave in the same way.
6 changes: 1 addition & 5 deletions mesonbuild/interpreter/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3368,11 +3368,7 @@ def build_library(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVararg
default_library = self.coredata.optstore.get_value_for(OptionKey('default_library', subproject=self.subproject))
assert isinstance(default_library, str), 'for mypy'
if default_library == 'shared':
# Intentionally pass shared_library_only=False so that dependencies
# end up in Requires.private. Many libraries that refer to their
# dependencies' in their headers expect this so that those dependencies
# are added to the output of 'pkgconfig --cflags'.
return self.build_target(node, args, T.cast('kwtypes.SharedLibrary', kwargs), build.SharedLibrary, shared_library_only=False)
return self.build_target(node, args, T.cast('kwtypes.SharedLibrary', kwargs), build.SharedLibrary)
elif default_library == 'static':
return self.build_target(node, args, T.cast('kwtypes.StaticLibrary', kwargs), build.StaticLibrary)
elif default_library == 'both':
Expand Down
65 changes: 49 additions & 16 deletions mesonbuild/modules/pkgconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class GenerateKw(TypedDict):
libraries: T.List[ANY_DEP]
libraries_private: T.List[ANY_DEP]
requires: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]
requires_shared: T.Optional[T.List[T.Union[str, dependencies.Dependency]]]
requires_private: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]
install_dir: T.Optional[str]
d_module_versions: T.List[T.Union[str, int]]
Expand Down Expand Up @@ -103,6 +104,7 @@ def __init__(self, state: ModuleState, name: str, metadata: T.Dict[str, MetaData
self.version_reqs: T.DefaultDict[str, T.Set[str]] = defaultdict(set)
self.link_whole_targets: T.List[T.Union[build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary]] = []
self.uninstalled_incdirs: mesonlib.OrderedSet[str] = mesonlib.OrderedSet()
self.shlib_needs_requires_private: bool = True

def add_pub_libs(self, libs: T.List[ANY_DEP]) -> None:
p_libs, reqs, cflags = self._process_libs(libs, True)
Expand All @@ -111,6 +113,12 @@ def add_pub_libs(self, libs: T.List[ANY_DEP]) -> None:
self.cflags += cflags

def add_priv_libs(self, libs: T.List[ANY_DEP]) -> None:
# FIXME: cflags from non-pkg-config ExternalDependency objects are
# discarded here. They should be added to self.cflags so that
# consumers get the right compile flags when headers require those
# non-pkg-config external dependencies. PkgConfigDependency is
# fine because its cflags come transitively via Requires.private.
# The same applies to add_shlib_deps below.
p_libs, reqs, _ = self._process_libs(libs, False)
self.priv_libs = p_libs + self.priv_libs
self.priv_reqs += reqs
Expand All @@ -121,6 +129,22 @@ def add_pub_reqs(self, reqs: T.List[T.Union[str, build.StaticLibrary, build.Shar
def add_priv_reqs(self, reqs: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]) -> None:
self.priv_reqs += self._process_reqs(reqs)

def add_shlib_deps(self, external_deps: T.List[dependencies.Dependency]) -> None:
if not self.shlib_needs_requires_private:
return
# A mix of add_priv_libs and add_priv_reqs. Like the
# shared_library_only=False case, Libs.private is not needed
# (consumers link to the .so, not its deps); but unlike
# add_priv_reqs, _process_libs does not raise an error for
# non-pkg-config ExternalDependency objects. This ensures that
# shared_library() and library() with default_library=shared
# produce the same Requires.private in the generated .pc file,
# but it is not entirely correct. See the comment above for
# add_priv_libs().
libs = T.cast('T.List[ANY_DEP]', external_deps)
_, reqs, _ = self._process_libs(libs, False)
self.priv_reqs += reqs

def _check_generated_pc_deprecation(self, obj: T.Union[build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary, build.SharedLibrary]) -> None:
if obj.get_id() in self.metadata:
return
Expand Down Expand Up @@ -232,25 +256,25 @@ def _process_libs(
if obj.found():
processed_libs += obj.get_link_args()
processed_cflags += obj.get_compile_args()
elif isinstance(obj, build.SharedLibrary) and obj.shared_library_only:
# Do not pull dependencies for shared libraries because they are
# only required for static linking. Adding private requires has
# the side effect of exposing their cflags, which is the
# intended behaviour of pkg-config but force Debian to add more
# than needed build deps.
# See https://bugs.freedesktop.org/show_bug.cgi?id=105572
processed_libs.append(obj)
self._add_uninstalled_incdirs(obj.get_include_dirs(), obj.get_subdir())
elif isinstance(obj, (build.SharedLibrary, build.StaticLibrary)):
processed_libs.append(obj)
self._add_uninstalled_incdirs(obj.get_include_dirs(), obj.get_subdir())
# If there is a static library in `Libs:` all its deps must be
# public too, otherwise the generated pc file will never be
# usable without --static.
self._add_lib_dependencies(obj.link_targets,
obj.link_whole_targets,
obj.external_deps,
isinstance(obj, build.StaticLibrary) and public)
if isinstance(obj, build.SharedLibrary) and obj.shared_library_only:
# Do not pull dependencies for shared libraries because they are
# only required for static linking. Requires.private
# are needed on the assumption that the headers need them,
# which is the intended behaviour of pkg-config though it
# forced Debian to add more than needed build deps.
# See https://bugs.freedesktop.org/show_bug.cgi?id=105572
self.add_shlib_deps(obj.external_deps)
else:
# If there is a static library in `Libs:` all its deps must be
# public too, otherwise the generated pc file will never be
# usable without --static.
self._add_lib_dependencies(obj.link_targets,
obj.link_whole_targets,
obj.external_deps,
isinstance(obj, build.StaticLibrary) and public)
elif isinstance(obj, (build.CustomTarget, build.CustomTargetIndex)):
if not obj.is_linkable_target():
raise mesonlib.MesonException('library argument contains a not linkable custom_target.')
Expand Down Expand Up @@ -679,6 +703,11 @@ def generate_libs_flags(libs: T.List[LIBS]) -> T.Iterable[str]:
_PKG_LIBRARIES.evolve(name='libraries_private'),
_PKG_REQUIRES,
_PKG_REQUIRES.evolve(name='requires_private'),
KwargInfo('requires_shared',
(ContainerTypeInfo(list, (str, dependencies.Dependency)), NoneType),
default=None,
listify=True,
since='1.11.0')
)
def generate(self, state: ModuleState,
args: T.Tuple[T.Optional[T.Union[build.SharedLibrary, build.StaticLibrary]]],
Expand Down Expand Up @@ -742,10 +771,14 @@ def generate(self, state: ModuleState,
libraries.insert(0, mainlib)

deps = DependenciesHelper(state, filebase, self._metadata)
if kwargs['requires_shared'] is not None:
deps.shlib_needs_requires_private = False
deps.add_pub_libs(libraries)
deps.add_priv_libs(kwargs['libraries_private'])
deps.add_pub_reqs(kwargs['requires'])
deps.add_priv_reqs(kwargs['requires_private'])
if kwargs['requires_shared'] is not None:
deps.add_priv_reqs(kwargs['requires_shared'])
deps.add_cflags(kwargs['extra_cflags'])
deps.add_cflags_private(kwargs['cflags_private'])

Expand Down
6 changes: 5 additions & 1 deletion test cases/unit/74 pkgconfig prefixes/val2/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ val2 = shared_library('val2', 'val2.c',
install: true)
install_headers('val2.h')
pkgconfig = import('pkgconfig')
pkgconfig.generate(val2, libraries : ['-Wl,-rpath,${libdir}'])

# linuxliketests.py will leave val1.pc out of the PKG_CONFIG_PATH,
# to check that requires_shared: [] removes it from Requires.private
pkgconfig.generate(val2, libraries : ['-Wl,-rpath,${libdir}'],
requires_shared: [])
26 changes: 23 additions & 3 deletions unittests/linuxliketests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,23 @@ def test_old_gnome_module_codepaths(self):
override_envvars=env)
self.build(override_envvars=env)

@skipIfNoPkgconfig
def test_pkgconfig_shlib_requires_private(self):
'''
Test that a shared library's external dependencies appear in
Requires.private of the generated .pc file, so that --cflags
exposes their compile args.
'''
testdir = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependency')
if subprocess.call([PKG_CONFIG, '--cflags', 'glib-2.0'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) != 0:
raise SkipTest('Glib 2.0 dependency not available.')
self.init(testdir)
with open(os.path.join(self.privatedir, 'libpkgdep.pc'), encoding='utf-8') as f:
content = f.read()
self.assertIn('Requires.private: glib-2.0', content)

@skipIfNoPkgconfig
def test_pkgconfig_usage(self):
testdir1 = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependency')
Expand All @@ -1134,9 +1151,10 @@ def test_pkgconfig_usage(self):
# Private internal libraries must not leak out.
pkg_out = subprocess.check_output([PKG_CONFIG, '--static', '--libs', 'libpkgdep'], env=myenv)
self.assertNotIn(b'libpkgdep-int', pkg_out, 'Internal library leaked out.')
# Dependencies must not leak to cflags when building only a shared library.
# Dependencies of a shared library should appear via Requires.private
# so that --cflags exposes their compile args (headers may need them).
pkg_out = subprocess.check_output([PKG_CONFIG, '--cflags', 'libpkgdep'], env=myenv)
self.assertNotIn(b'glib', pkg_out, 'Internal dependency leaked to headers.')
self.assertIn(b'glib', pkg_out, 'Shared library dependency not exposed via cflags.')
# Test that the result is usable.
self.init(testdir2, override_envvars=myenv)
self.build(override_envvars=myenv)
Expand Down Expand Up @@ -1476,7 +1494,9 @@ def test_usage_pkgconfig_prefixes(self):
self.install(use_destdir=False)
self.new_builddir()

# Build, install, and run the client program
# Build, install, and run the client program. val2 uses
# requires_shared to suppress the automatic Requires.private for
# val1, so only val2's pkgconfig path is needed.
env2 = {}
env2['PKG_CONFIG_PATH'] = os.path.join(val2prefix, self.libdir, 'pkgconfig')
testdir = os.path.join(self.unit_test_dir, '74 pkgconfig prefixes', 'client')
Expand Down
Loading