Skip to content

Commit 5bdd3d5

Browse files
feat(cmake): add installation support for pkg-config dependency detection (pybind#4077)
* add installation support for pkg-config dependency detection pkg-config is a buildsystem-agnostic alternative to `pybind11Config.cmake` that can be used from build systems other than cmake. Fixes pybind#230 * tests: add test for pkg config Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Henry Schreiner <[email protected]>
1 parent 14c8465 commit 5bdd3d5

12 files changed

+143
-62
lines changed

.pre-commit-config.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#
1313
# See https://github.com/pre-commit/pre-commit
1414

15+
# third-party content
16+
exclude: ^tools/JoinPaths.cmake$
17+
1518
repos:
1619
# Standard hooks
1720
- repo: https://github.com/pre-commit/pre-commit-hooks

CMakeLists.txt

+13
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ else()
198198
endif()
199199

200200
include("${CMAKE_CURRENT_SOURCE_DIR}/tools/pybind11Common.cmake")
201+
# https://github.com/jtojnar/cmake-snips/#concatenating-paths-when-building-pkg-config-files
202+
# TODO: cmake 3.20 adds the cmake_path() function, which obsoletes this snippet
203+
include("${CMAKE_CURRENT_SOURCE_DIR}/tools/JoinPaths.cmake")
201204

202205
# Relative directory setting
203206
if(USE_PYTHON_INCLUDE_DIR AND DEFINED Python_INCLUDE_DIRS)
@@ -262,6 +265,16 @@ if(PYBIND11_INSTALL)
262265
NAMESPACE "pybind11::"
263266
DESTINATION ${PYBIND11_CMAKECONFIG_INSTALL_DIR})
264267

268+
# pkg-config support
269+
if(NOT prefix_for_pc_file)
270+
set(prefix_for_pc_file "${CMAKE_INSTALL_PREFIX}")
271+
endif()
272+
join_paths(includedir_for_pc_file "\${prefix}" "${CMAKE_INSTALL_INCLUDEDIR}")
273+
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/tools/pybind11.pc.in"
274+
"${CMAKE_CURRENT_BINARY_DIR}/pybind11.pc" @ONLY)
275+
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pybind11.pc"
276+
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig/")
277+
265278
# Uninstall target
266279
if(PYBIND11_MASTER_PROJECT)
267280
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake_uninstall.cmake.in"

noxfile.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def lint(session: nox.Session) -> None:
2727
Lint the codebase (except for clang-format/tidy).
2828
"""
2929
session.install("pre-commit")
30-
session.run("pre-commit", "run", "-a")
30+
session.run("pre-commit", "run", "-a", *session.posargs)
3131

3232

3333
@nox.session(python=PYTHON_VERSIONS)
@@ -58,7 +58,7 @@ def tests_packaging(session: nox.Session) -> None:
5858
"""
5959

6060
session.install("-r", "tests/requirements.txt", "--prefer-binary")
61-
session.run("pytest", "tests/extra_python_package")
61+
session.run("pytest", "tests/extra_python_package", *session.posargs)
6262

6363

6464
@nox.session(reuse_venv=True)

pybind11/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77

88
from ._version import __version__, version_info
9-
from .commands import get_cmake_dir, get_include
9+
from .commands import get_cmake_dir, get_include, get_pkgconfig_dir
1010

1111
__all__ = (
1212
"version_info",
1313
"__version__",
1414
"get_include",
1515
"get_cmake_dir",
16+
"get_pkgconfig_dir",
1617
)

pybind11/__main__.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import sysconfig
66

7-
from .commands import get_cmake_dir, get_include
7+
from .commands import get_cmake_dir, get_include, get_pkgconfig_dir
88

99

1010
def print_includes() -> None:
@@ -36,13 +36,20 @@ def main() -> None:
3636
action="store_true",
3737
help="Print the CMake module directory, ideal for setting -Dpybind11_ROOT in CMake.",
3838
)
39+
parser.add_argument(
40+
"--pkgconfigdir",
41+
action="store_true",
42+
help="Print the pkgconfig directory, ideal for setting $PKG_CONFIG_PATH.",
43+
)
3944
args = parser.parse_args()
4045
if not sys.argv[1:]:
4146
parser.print_help()
4247
if args.includes:
4348
print_includes()
4449
if args.cmakedir:
4550
print(get_cmake_dir())
51+
if args.pkgconfigdir:
52+
print(get_pkgconfig_dir())
4653

4754

4855
if __name__ == "__main__":

pybind11/commands.py

+12
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,15 @@ def get_cmake_dir() -> str:
2323

2424
msg = "pybind11 not installed, installation required to access the CMake files"
2525
raise ImportError(msg)
26+
27+
28+
def get_pkgconfig_dir() -> str:
29+
"""
30+
Return the path to the pybind11 pkgconfig directory.
31+
"""
32+
pkgconfig_installed_path = os.path.join(DIR, "share", "pkgconfig")
33+
if os.path.exists(pkgconfig_installed_path):
34+
return pkgconfig_installed_path
35+
36+
msg = "pybind11 not installed, installation required to access the pkgconfig files"
37+
raise ImportError(msg)

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def remove_output(*sources: str) -> Iterator[None]:
127127
"-DCMAKE_INSTALL_PREFIX=pybind11",
128128
"-DBUILD_TESTING=OFF",
129129
"-DPYBIND11_NOPYTHON=ON",
130+
"-Dprefix_for_pc_file=${pcfiledir}/../../",
130131
]
131132
if "CMAKE_ARGS" in os.environ:
132133
fcommand = [

tests/extra_python_package/test_files.py

+68-58
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@
1212
DIR = os.path.abspath(os.path.dirname(__file__))
1313
MAIN_DIR = os.path.dirname(os.path.dirname(DIR))
1414

15+
PKGCONFIG = """\
16+
prefix=${{pcfiledir}}/../../
17+
includedir=${{prefix}}/include
18+
19+
Name: pybind11
20+
Description: Seamless operability between C++11 and Python
21+
Version: {VERSION}
22+
Cflags: -I${{includedir}}
23+
"""
24+
1525

1626
main_headers = {
1727
"include/pybind11/attr.h",
@@ -59,6 +69,10 @@
5969
"share/cmake/pybind11/pybind11Tools.cmake",
6070
}
6171

72+
pkgconfig_files = {
73+
"share/pkgconfig/pybind11.pc",
74+
}
75+
6276
py_files = {
6377
"__init__.py",
6478
"__main__.py",
@@ -69,7 +83,7 @@
6983
}
7084

7185
headers = main_headers | detail_headers | stl_headers
72-
src_files = headers | cmake_files
86+
src_files = headers | cmake_files | pkgconfig_files
7387
all_files = src_files | py_files
7488

7589

@@ -82,6 +96,7 @@
8296
"pybind11/share",
8397
"pybind11/share/cmake",
8498
"pybind11/share/cmake/pybind11",
99+
"pybind11/share/pkgconfig",
85100
"pyproject.toml",
86101
"setup.cfg",
87102
"setup.py",
@@ -101,22 +116,25 @@
101116
}
102117

103118

119+
def read_tz_file(tar: tarfile.TarFile, name: str) -> bytes:
120+
start = tar.getnames()[0] + "/"
121+
inner_file = tar.extractfile(tar.getmember(f"{start}{name}"))
122+
assert inner_file
123+
with contextlib.closing(inner_file) as f:
124+
return f.read()
125+
126+
127+
def normalize_line_endings(value: bytes) -> bytes:
128+
return value.replace(os.linesep.encode("utf-8"), b"\n")
129+
130+
104131
def test_build_sdist(monkeypatch, tmpdir):
105132

106133
monkeypatch.chdir(MAIN_DIR)
107134

108-
out = subprocess.check_output(
109-
[
110-
sys.executable,
111-
"-m",
112-
"build",
113-
"--sdist",
114-
"--outdir",
115-
str(tmpdir),
116-
]
135+
subprocess.run(
136+
[sys.executable, "-m", "build", "--sdist", f"--outdir={tmpdir}"], check=True
117137
)
118-
if hasattr(out, "decode"):
119-
out = out.decode()
120138

121139
(sdist,) = tmpdir.visit("*.tar.gz")
122140

@@ -125,25 +143,17 @@ def test_build_sdist(monkeypatch, tmpdir):
125143
version = start[9:-1]
126144
simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]}
127145

128-
with contextlib.closing(
129-
tar.extractfile(tar.getmember(start + "setup.py"))
130-
) as f:
131-
setup_py = f.read()
132-
133-
with contextlib.closing(
134-
tar.extractfile(tar.getmember(start + "pyproject.toml"))
135-
) as f:
136-
pyproject_toml = f.read()
137-
138-
with contextlib.closing(
139-
tar.extractfile(
140-
tar.getmember(
141-
start + "pybind11/share/cmake/pybind11/pybind11Config.cmake"
142-
)
143-
)
144-
) as f:
145-
contents = f.read().decode("utf8")
146-
assert 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")' in contents
146+
setup_py = read_tz_file(tar, "setup.py")
147+
pyproject_toml = read_tz_file(tar, "pyproject.toml")
148+
pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc")
149+
cmake_cfg = read_tz_file(
150+
tar, "pybind11/share/cmake/pybind11/pybind11Config.cmake"
151+
)
152+
153+
assert (
154+
'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")'
155+
in cmake_cfg.decode("utf-8")
156+
)
147157

148158
files = {f"pybind11/{n}" for n in all_files}
149159
files |= sdist_files
@@ -154,51 +164,47 @@ def test_build_sdist(monkeypatch, tmpdir):
154164

155165
with open(os.path.join(MAIN_DIR, "tools", "setup_main.py.in"), "rb") as f:
156166
contents = (
157-
string.Template(f.read().decode())
167+
string.Template(f.read().decode("utf-8"))
158168
.substitute(version=version, extra_cmd="")
159-
.encode()
169+
.encode("utf-8")
160170
)
161171
assert setup_py == contents
162172

163173
with open(os.path.join(MAIN_DIR, "tools", "pyproject.toml"), "rb") as f:
164174
contents = f.read()
165175
assert pyproject_toml == contents
166176

177+
simple_version = ".".join(version.split(".")[:3])
178+
pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version).encode("utf-8")
179+
assert normalize_line_endings(pkgconfig) == pkgconfig_expected
180+
167181

168182
def test_build_global_dist(monkeypatch, tmpdir):
169183

170184
monkeypatch.chdir(MAIN_DIR)
171185
monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1")
172-
out = subprocess.check_output(
173-
[
174-
sys.executable,
175-
"-m",
176-
"build",
177-
"--sdist",
178-
"--outdir",
179-
str(tmpdir),
180-
]
186+
subprocess.run(
187+
[sys.executable, "-m", "build", "--sdist", "--outdir", str(tmpdir)], check=True
181188
)
182189

183-
if hasattr(out, "decode"):
184-
out = out.decode()
185-
186190
(sdist,) = tmpdir.visit("*.tar.gz")
187191

188192
with tarfile.open(str(sdist), "r:gz") as tar:
189193
start = tar.getnames()[0] + "/"
190194
version = start[16:-1]
191195
simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]}
192196

193-
with contextlib.closing(
194-
tar.extractfile(tar.getmember(start + "setup.py"))
195-
) as f:
196-
setup_py = f.read()
197+
setup_py = read_tz_file(tar, "setup.py")
198+
pyproject_toml = read_tz_file(tar, "pyproject.toml")
199+
pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc")
200+
cmake_cfg = read_tz_file(
201+
tar, "pybind11/share/cmake/pybind11/pybind11Config.cmake"
202+
)
197203

198-
with contextlib.closing(
199-
tar.extractfile(tar.getmember(start + "pyproject.toml"))
200-
) as f:
201-
pyproject_toml = f.read()
204+
assert (
205+
'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")'
206+
in cmake_cfg.decode("utf-8")
207+
)
202208

203209
files = {f"pybind11/{n}" for n in all_files}
204210
files |= sdist_files
@@ -209,20 +215,24 @@ def test_build_global_dist(monkeypatch, tmpdir):
209215
contents = (
210216
string.Template(f.read().decode())
211217
.substitute(version=version, extra_cmd="")
212-
.encode()
218+
.encode("utf-8")
213219
)
214220
assert setup_py == contents
215221

216222
with open(os.path.join(MAIN_DIR, "tools", "pyproject.toml"), "rb") as f:
217223
contents = f.read()
218224
assert pyproject_toml == contents
219225

226+
simple_version = ".".join(version.split(".")[:3])
227+
pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version).encode("utf-8")
228+
assert normalize_line_endings(pkgconfig) == pkgconfig_expected
229+
220230

221231
def tests_build_wheel(monkeypatch, tmpdir):
222232
monkeypatch.chdir(MAIN_DIR)
223233

224-
subprocess.check_output(
225-
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)]
234+
subprocess.run(
235+
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)], check=True
226236
)
227237

228238
(wheel,) = tmpdir.visit("*.whl")
@@ -249,8 +259,8 @@ def tests_build_global_wheel(monkeypatch, tmpdir):
249259
monkeypatch.chdir(MAIN_DIR)
250260
monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1")
251261

252-
subprocess.check_output(
253-
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)]
262+
subprocess.run(
263+
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)], check=True
254264
)
255265

256266
(wheel,) = tmpdir.visit("*.whl")

tools/JoinPaths.cmake

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# This module provides function for joining paths
2+
# known from most languages
3+
#
4+
# SPDX-License-Identifier: (MIT OR CC0-1.0)
5+
# Copyright 2020 Jan Tojnar
6+
# https://github.com/jtojnar/cmake-snips
7+
#
8+
# Modelled after Python’s os.path.join
9+
# https://docs.python.org/3.7/library/os.path.html#os.path.join
10+
# Windows not supported
11+
function(join_paths joined_path first_path_segment)
12+
set(temp_path "${first_path_segment}")
13+
foreach(current_segment IN LISTS ARGN)
14+
if(NOT ("${current_segment}" STREQUAL ""))
15+
if(IS_ABSOLUTE "${current_segment}")
16+
set(temp_path "${current_segment}")
17+
else()
18+
set(temp_path "${temp_path}/${current_segment}")
19+
endif()
20+
endif()
21+
endforeach()
22+
set(${joined_path} "${temp_path}" PARENT_SCOPE)
23+
endfunction()

tools/pybind11.pc.in

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
prefix=@prefix_for_pc_file@
2+
includedir=@includedir_for_pc_file@
3+
4+
Name: @PROJECT_NAME@
5+
Description: Seamless operability between C++11 and Python
6+
Version: @PROJECT_VERSION@
7+
Cflags: -I${includedir}

tools/setup_global.py.in

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ main_headers = glob.glob("pybind11/include/pybind11/*.h")
2929
detail_headers = glob.glob("pybind11/include/pybind11/detail/*.h")
3030
stl_headers = glob.glob("pybind11/include/pybind11/stl/*.h")
3131
cmake_files = glob.glob("pybind11/share/cmake/pybind11/*.cmake")
32+
pkgconfig_files = glob.glob("pybind11/share/pkgconfig/*.pc")
3233
headers = main_headers + detail_headers + stl_headers
3334

3435
cmdclass = {"install_headers": InstallHeadersNested}
@@ -51,6 +52,7 @@ setup(
5152
headers=headers,
5253
data_files=[
5354
(base + "share/cmake/pybind11", cmake_files),
55+
(base + "share/pkgconfig", pkgconfig_files),
5456
(base + "include/pybind11", main_headers),
5557
(base + "include/pybind11/detail", detail_headers),
5658
(base + "include/pybind11/stl", stl_headers),

0 commit comments

Comments
 (0)