From f8bff8192579719dac89164b43276652943b4cbe Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Sat, 27 Oct 2018 14:36:24 -0400 Subject: [PATCH] pkgconfig: Generate -uninstalled.pc files Closes: #3472. --- docs/markdown/Pkgconfig-module.md | 11 +++ .../snippets/uninstalled-pkgconfig.md | 8 ++ mesonbuild/modules/pkgconfig.py | 86 +++++++++++++++---- run_unittests.py | 15 ++++ .../48 pkgconfig-gen/dependencies/main.c | 6 ++ .../48 pkgconfig-gen/dependencies/meson.build | 3 + .../common/48 pkgconfig-gen/meson.build | 1 - 7 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 docs/markdown/snippets/uninstalled-pkgconfig.md create mode 100644 test cases/common/48 pkgconfig-gen/dependencies/main.c diff --git a/docs/markdown/Pkgconfig-module.md b/docs/markdown/Pkgconfig-module.md index 7e935247d551..bb230de726c5 100644 --- a/docs/markdown/Pkgconfig-module.md +++ b/docs/markdown/Pkgconfig-module.md @@ -54,6 +54,8 @@ keyword arguments. `Version:` field. Defaults to the project version if unspecified. - `d_module_versions` a list of module version flags used when compiling D sources referred to by this pkg-config file +- `uninstalled_variables` used instead of the `variables` keyword argument, when + generating the uninstalled pkg-config file. Since *0.49.0* Since 0.46 a `StaticLibrary` or `SharedLibrary` object can optionally be passed as first positional argument. If one is provided a default value will be @@ -62,6 +64,15 @@ provided for all required fields of the pc file: - `description` is set to the project's name followed by the library's name. - `name` is set to the library's name. +Since 0.49.0 uninstalled pkg-config files are generated as well. They are +located in `/meson-uninstalled/`. It is sometimes +useful to build projects against libraries built by meson without having to +install them into a prefix. In order to do so, just set +`PKG_CONFIG_PATH=/meson-uninstalled` before building your +application. That will cause pkg-config to prefer those `-uninstalled.pc` files +and find libraries and headers from the meson builddir. This is an experimental +feature provided on a best-effort basis, it might not work in all use-cases. + ### Implicit dependencies The exact rules followed to find dependencies that are implicitly added into the diff --git a/docs/markdown/snippets/uninstalled-pkgconfig.md b/docs/markdown/snippets/uninstalled-pkgconfig.md new file mode 100644 index 000000000000..2e265ab419ae --- /dev/null +++ b/docs/markdown/snippets/uninstalled-pkgconfig.md @@ -0,0 +1,8 @@ +## Uninstalled pkg-config files + +The `pkgconfig` module now generates uninstalled pc files as well. For any generated +`foo.pc` file, an extra `foo-uninstalled.pc` file is placed into +`/meson-uninstalled`. They can be used to build applications against +libraries built by meson without installing them, by pointing `PKG_CONFIG_PATH` +to that directory. This is an experimental feature provided on a best-effort +basis, it might not work in all use-cases. diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py index bc9bff8dd4e8..285673b5e3d9 100644 --- a/mesonbuild/modules/pkgconfig.py +++ b/mesonbuild/modules/pkgconfig.py @@ -210,6 +210,9 @@ def _fn(xs, libs=False): self.priv_reqs = [i for i in self.priv_reqs if i not in self.pub_reqs] class PkgConfigModule(ExtensionModule): + def __init__(self, interpreter): + super().__init__(interpreter) + self.uninstalled_info = [] def _get_lname(self, l, msg, pcfile): # Nothing special @@ -251,19 +254,30 @@ def _make_relative(self, prefix, subdir): return subdir def generate_pkgconfig_file(self, state, deps, subdirs, name, description, - url, version, pcfile, conflicts, variables): + url, version, pcfile, conflicts, variables, + uninstalled=False): deps.remove_dups() coredata = state.environment.get_coredata() - outdir = state.environment.scratch_dir - fname = os.path.join(outdir, pcfile) - prefix = PurePath(coredata.get_builtin_option('prefix')) - # These always return paths relative to prefix - libdir = PurePath(coredata.get_builtin_option('libdir')) - incdir = PurePath(coredata.get_builtin_option('includedir')) + if not uninstalled: + fname = os.path.join(state.environment.scratch_dir, pcfile) + else: + outdir = os.path.join(state.environment.build_dir, 'meson-uninstalled') + if not os.path.exists(outdir): + os.mkdir(outdir) + fname = os.path.join(outdir, pcfile) with open(fname, 'w') as ofile: - ofile.write('prefix={}\n'.format(self._escape(prefix))) - ofile.write('libdir={}\n'.format(self._escape('${prefix}' / libdir))) - ofile.write('includedir={}\n'.format(self._escape('${prefix}' / incdir))) + if not uninstalled: + prefix = PurePath(coredata.get_builtin_option('prefix')) + # These always return paths relative to prefix + libdir = PurePath(coredata.get_builtin_option('libdir')) + incdir = PurePath(coredata.get_builtin_option('includedir')) + ofile.write('prefix={}\n'.format(self._escape(prefix))) + ofile.write('libdir={}\n'.format(self._escape('${prefix}' / libdir))) + ofile.write('includedir={}\n'.format(self._escape('${prefix}' / incdir))) + else: + ofile.write('prefix=\n') + ofile.write('libdir=\n') + ofile.write('includedir=\n') if variables: ofile.write('\n') for k, v in variables: @@ -292,6 +306,11 @@ def generate_libs_flags(libs): for l in libs: if isinstance(l, str): yield l + elif uninstalled: + path = state.backend.get_target_filename_abs(l) + if path not in Lflags: + Lflags.append(path) + yield path else: install_dir = l.get_custom_install_dir()[0] if install_dir is False: @@ -317,22 +336,41 @@ def generate_libs_flags(libs): if 'cs' not in l.compilers: yield '-l%s' % lname + def generate_uninstalled_cflags(): + result = [] + for h in state.headers: + if h.subproject != state.subproject: + continue + for f in h.sources: + if not isinstance(f, mesonlib.File): + continue + abspath = f.absolute_path(state.environment.get_source_dir(), + state.environment.get_build_dir()) + incdir = os.path.dirname(abspath) + if incdir not in result: + result.append(incdir) + yield '-I%s' % self._escape(incdir) + if len(deps.pub_libs) > 0: ofile.write('Libs: {}\n'.format(' '.join(generate_libs_flags(deps.pub_libs)))) if len(deps.priv_libs) > 0: ofile.write('Libs.private: {}\n'.format(' '.join(generate_libs_flags(deps.priv_libs)))) ofile.write('Cflags:') - for h in subdirs: - ofile.write(' ') - if h == '.': - ofile.write('-I${includedir}') - else: - ofile.write(self._escape(PurePath('-I${includedir}') / h)) + if uninstalled: + ofile.write(' '.join(generate_uninstalled_cflags())) + else: + for h in subdirs: + ofile.write(' ') + if h == '.': + ofile.write('-I${includedir}') + else: + ofile.write(self._escape(PurePath('-I${includedir}') / h)) for f in deps.cflags: ofile.write(' ') ofile.write(self._escape(f)) ofile.write('\n') + @FeatureNewKwargs('pkgconfig.generate', '0.49.0', ['uninstalled_variables']) @FeatureNewKwargs('pkgconfig.generate', '0.42.0', ['extra_cflags']) @FeatureNewKwargs('pkgconfig.generate', '0.41.0', ['variables']) @permittedKwargs({'libraries', 'version', 'name', 'description', 'filebase', @@ -427,6 +465,15 @@ def parse_variable_list(stringlist): self.generate_pkgconfig_file(state, deps, subdirs, name, description, url, version, pcfile, conflicts, variables) res = build.Data(mesonlib.File(True, state.environment.get_scratch_dir(), pcfile), pkgroot) + variables = parse_variable_list(mesonlib.stringlistify(kwargs.get('uninstalled_variables', []))) + pcfile = filebase + '-uninstalled.pc' + # postpone generation of uninstalled pc files because we need the complete list + # of installed headers, and user could call install_headers() after generating + # the pc file. Keep all needed information into a list that will be processed + # when interpreter calls _finish_project() method. + generate_pkgconfig_file_args = (deps, subdirs, name, description, url, + version, pcfile, conflicts, variables) + self.uninstalled_info.append((state.subproject, generate_pkgconfig_file_args)) # Associate the main library with this generated pc file. If the library # is used in any subsequent call to the generated, it will generate a # 'Requires:' or 'Requires.private:'. @@ -445,5 +492,12 @@ def parse_variable_list(stringlist): lineno=state.current_lineno) return ModuleReturnValue(res, [res]) + # Called by interpreter when it's done parsing a (sub)project + def _finish_project(self, state): + for info in self.uninstalled_info: + subproject, args = info + if state.subproject == subproject: + self.generate_pkgconfig_file(state, *args, uninstalled=True) + def initialize(*args, **kwargs): return PkgConfigModule(*args, **kwargs) diff --git a/run_unittests.py b/run_unittests.py index 55a5bd6d267e..6ef00a732973 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -3910,6 +3910,21 @@ def test_pkgconfig_gen_deps(self): out = self._run(cmd + ['--print-requires-private']).strip().split('\n') self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo >= 1.0', 'libhello'])) + def test_pkgconfig_uninstalled(self): + testdir = os.path.join(self.common_test_dir, '48 pkgconfig-gen') + self.init(testdir) + self.build() + + os.environ['PKG_CONFIG_LIBDIR'] = os.path.join(self.builddir, 'meson-uninstalled') + if is_cygwin(): + os.environ['PATH'] += os.pathsep + self.builddir + + self.new_builddir() + testdir = os.path.join(self.common_test_dir, '48 pkgconfig-gen', 'dependencies') + self.init(testdir) + self.build() + self.run_tests() + def test_pkg_unfound(self): testdir = os.path.join(self.unit_test_dir, '23 unfound pkgconfig') self.init(testdir) diff --git a/test cases/common/48 pkgconfig-gen/dependencies/main.c b/test cases/common/48 pkgconfig-gen/dependencies/main.c new file mode 100644 index 000000000000..61708d386a1f --- /dev/null +++ b/test cases/common/48 pkgconfig-gen/dependencies/main.c @@ -0,0 +1,6 @@ +#include + +int main(int argc, char *argv[]) +{ + return simple_function() == 42 ? 0 : 1; +} diff --git a/test cases/common/48 pkgconfig-gen/dependencies/meson.build b/test cases/common/48 pkgconfig-gen/dependencies/meson.build index c72f96b44f3d..8e7895752a6b 100644 --- a/test cases/common/48 pkgconfig-gen/dependencies/meson.build +++ b/test cases/common/48 pkgconfig-gen/dependencies/meson.build @@ -18,6 +18,9 @@ threads_dep = dependency('threads') custom_dep = declare_dependency(link_with : custom_lib, compile_args : ['-DCUSTOM']) custom2_dep = declare_dependency(link_args : ['-lcustom2'], compile_args : ['-DCUSTOM2']) +exe = executable('test1', 'main.c', dependencies : [pc_dep]) +test('Test1', exe) + # Generate a PC file: # - Having libmain in libraries should pull implicitly libexposed and libinternal in Libs.private # - Having libexposed in libraries should remove it from Libs.private diff --git a/test cases/common/48 pkgconfig-gen/meson.build b/test cases/common/48 pkgconfig-gen/meson.build index 7e6c67037ea1..09c46c515a61 100644 --- a/test cases/common/48 pkgconfig-gen/meson.build +++ b/test cases/common/48 pkgconfig-gen/meson.build @@ -1,7 +1,6 @@ project('pkgconfig-gen', 'c') # First check we have pkg-config >= 0.29 - pkgconfig = find_program('pkg-config', required: false) if not pkgconfig.found() error('MESON_SKIP_TEST: pkg-config not found')