feat(cmake): add installation support for pkg-config dependency detection (#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 #230

* tests: add test for pkg config

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
This commit is contained in:
Eli Schwartz 2022-08-09 00:02:45 -04:00 committed by GitHub
parent 14c84654f8
commit 5bdd3d59be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 142 additions and 61 deletions

View File

@ -12,6 +12,9 @@
# #
# See https://github.com/pre-commit/pre-commit # See https://github.com/pre-commit/pre-commit
# third-party content
exclude: ^tools/JoinPaths.cmake$
repos: repos:
# Standard hooks # Standard hooks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks

View File

@ -198,6 +198,9 @@ else()
endif() endif()
include("${CMAKE_CURRENT_SOURCE_DIR}/tools/pybind11Common.cmake") include("${CMAKE_CURRENT_SOURCE_DIR}/tools/pybind11Common.cmake")
# https://github.com/jtojnar/cmake-snips/#concatenating-paths-when-building-pkg-config-files
# TODO: cmake 3.20 adds the cmake_path() function, which obsoletes this snippet
include("${CMAKE_CURRENT_SOURCE_DIR}/tools/JoinPaths.cmake")
# Relative directory setting # Relative directory setting
if(USE_PYTHON_INCLUDE_DIR AND DEFINED Python_INCLUDE_DIRS) if(USE_PYTHON_INCLUDE_DIR AND DEFINED Python_INCLUDE_DIRS)
@ -262,6 +265,16 @@ if(PYBIND11_INSTALL)
NAMESPACE "pybind11::" NAMESPACE "pybind11::"
DESTINATION ${PYBIND11_CMAKECONFIG_INSTALL_DIR}) DESTINATION ${PYBIND11_CMAKECONFIG_INSTALL_DIR})
# pkg-config support
if(NOT prefix_for_pc_file)
set(prefix_for_pc_file "${CMAKE_INSTALL_PREFIX}")
endif()
join_paths(includedir_for_pc_file "\${prefix}" "${CMAKE_INSTALL_INCLUDEDIR}")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/tools/pybind11.pc.in"
"${CMAKE_CURRENT_BINARY_DIR}/pybind11.pc" @ONLY)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pybind11.pc"
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig/")
# Uninstall target # Uninstall target
if(PYBIND11_MASTER_PROJECT) if(PYBIND11_MASTER_PROJECT)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake_uninstall.cmake.in" configure_file("${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake_uninstall.cmake.in"

View File

@ -27,7 +27,7 @@ def lint(session: nox.Session) -> None:
Lint the codebase (except for clang-format/tidy). Lint the codebase (except for clang-format/tidy).
""" """
session.install("pre-commit") session.install("pre-commit")
session.run("pre-commit", "run", "-a") session.run("pre-commit", "run", "-a", *session.posargs)
@nox.session(python=PYTHON_VERSIONS) @nox.session(python=PYTHON_VERSIONS)
@ -58,7 +58,7 @@ def tests_packaging(session: nox.Session) -> None:
""" """
session.install("-r", "tests/requirements.txt", "--prefer-binary") session.install("-r", "tests/requirements.txt", "--prefer-binary")
session.run("pytest", "tests/extra_python_package") session.run("pytest", "tests/extra_python_package", *session.posargs)
@nox.session(reuse_venv=True) @nox.session(reuse_venv=True)

View File

@ -6,11 +6,12 @@ if sys.version_info < (3, 6):
from ._version import __version__, version_info from ._version import __version__, version_info
from .commands import get_cmake_dir, get_include from .commands import get_cmake_dir, get_include, get_pkgconfig_dir
__all__ = ( __all__ = (
"version_info", "version_info",
"__version__", "__version__",
"get_include", "get_include",
"get_cmake_dir", "get_cmake_dir",
"get_pkgconfig_dir",
) )

View File

@ -4,7 +4,7 @@ import argparse
import sys import sys
import sysconfig import sysconfig
from .commands import get_cmake_dir, get_include from .commands import get_cmake_dir, get_include, get_pkgconfig_dir
def print_includes() -> None: def print_includes() -> None:
@ -36,6 +36,11 @@ def main() -> None:
action="store_true", action="store_true",
help="Print the CMake module directory, ideal for setting -Dpybind11_ROOT in CMake.", help="Print the CMake module directory, ideal for setting -Dpybind11_ROOT in CMake.",
) )
parser.add_argument(
"--pkgconfigdir",
action="store_true",
help="Print the pkgconfig directory, ideal for setting $PKG_CONFIG_PATH.",
)
args = parser.parse_args() args = parser.parse_args()
if not sys.argv[1:]: if not sys.argv[1:]:
parser.print_help() parser.print_help()
@ -43,6 +48,8 @@ def main() -> None:
print_includes() print_includes()
if args.cmakedir: if args.cmakedir:
print(get_cmake_dir()) print(get_cmake_dir())
if args.pkgconfigdir:
print(get_pkgconfig_dir())
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -23,3 +23,15 @@ def get_cmake_dir() -> str:
msg = "pybind11 not installed, installation required to access the CMake files" msg = "pybind11 not installed, installation required to access the CMake files"
raise ImportError(msg) raise ImportError(msg)
def get_pkgconfig_dir() -> str:
"""
Return the path to the pybind11 pkgconfig directory.
"""
pkgconfig_installed_path = os.path.join(DIR, "share", "pkgconfig")
if os.path.exists(pkgconfig_installed_path):
return pkgconfig_installed_path
msg = "pybind11 not installed, installation required to access the pkgconfig files"
raise ImportError(msg)

View File

@ -127,6 +127,7 @@ with remove_output("pybind11/include", "pybind11/share"):
"-DCMAKE_INSTALL_PREFIX=pybind11", "-DCMAKE_INSTALL_PREFIX=pybind11",
"-DBUILD_TESTING=OFF", "-DBUILD_TESTING=OFF",
"-DPYBIND11_NOPYTHON=ON", "-DPYBIND11_NOPYTHON=ON",
"-Dprefix_for_pc_file=${pcfiledir}/../../",
] ]
if "CMAKE_ARGS" in os.environ: if "CMAKE_ARGS" in os.environ:
fcommand = [ fcommand = [

View File

@ -12,6 +12,16 @@ import zipfile
DIR = os.path.abspath(os.path.dirname(__file__)) DIR = os.path.abspath(os.path.dirname(__file__))
MAIN_DIR = os.path.dirname(os.path.dirname(DIR)) MAIN_DIR = os.path.dirname(os.path.dirname(DIR))
PKGCONFIG = """\
prefix=${{pcfiledir}}/../../
includedir=${{prefix}}/include
Name: pybind11
Description: Seamless operability between C++11 and Python
Version: {VERSION}
Cflags: -I${{includedir}}
"""
main_headers = { main_headers = {
"include/pybind11/attr.h", "include/pybind11/attr.h",
@ -59,6 +69,10 @@ cmake_files = {
"share/cmake/pybind11/pybind11Tools.cmake", "share/cmake/pybind11/pybind11Tools.cmake",
} }
pkgconfig_files = {
"share/pkgconfig/pybind11.pc",
}
py_files = { py_files = {
"__init__.py", "__init__.py",
"__main__.py", "__main__.py",
@ -69,7 +83,7 @@ py_files = {
} }
headers = main_headers | detail_headers | stl_headers headers = main_headers | detail_headers | stl_headers
src_files = headers | cmake_files src_files = headers | cmake_files | pkgconfig_files
all_files = src_files | py_files all_files = src_files | py_files
@ -82,6 +96,7 @@ sdist_files = {
"pybind11/share", "pybind11/share",
"pybind11/share/cmake", "pybind11/share/cmake",
"pybind11/share/cmake/pybind11", "pybind11/share/cmake/pybind11",
"pybind11/share/pkgconfig",
"pyproject.toml", "pyproject.toml",
"setup.cfg", "setup.cfg",
"setup.py", "setup.py",
@ -101,22 +116,25 @@ local_sdist_files = {
} }
def read_tz_file(tar: tarfile.TarFile, name: str) -> bytes:
start = tar.getnames()[0] + "/"
inner_file = tar.extractfile(tar.getmember(f"{start}{name}"))
assert inner_file
with contextlib.closing(inner_file) as f:
return f.read()
def normalize_line_endings(value: bytes) -> bytes:
return value.replace(os.linesep.encode("utf-8"), b"\n")
def test_build_sdist(monkeypatch, tmpdir): def test_build_sdist(monkeypatch, tmpdir):
monkeypatch.chdir(MAIN_DIR) monkeypatch.chdir(MAIN_DIR)
out = subprocess.check_output( subprocess.run(
[ [sys.executable, "-m", "build", "--sdist", f"--outdir={tmpdir}"], check=True
sys.executable,
"-m",
"build",
"--sdist",
"--outdir",
str(tmpdir),
]
) )
if hasattr(out, "decode"):
out = out.decode()
(sdist,) = tmpdir.visit("*.tar.gz") (sdist,) = tmpdir.visit("*.tar.gz")
@ -125,25 +143,17 @@ def test_build_sdist(monkeypatch, tmpdir):
version = start[9:-1] version = start[9:-1]
simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]} simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]}
with contextlib.closing( setup_py = read_tz_file(tar, "setup.py")
tar.extractfile(tar.getmember(start + "setup.py")) pyproject_toml = read_tz_file(tar, "pyproject.toml")
) as f: pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc")
setup_py = f.read() cmake_cfg = read_tz_file(
tar, "pybind11/share/cmake/pybind11/pybind11Config.cmake"
with contextlib.closing(
tar.extractfile(tar.getmember(start + "pyproject.toml"))
) as f:
pyproject_toml = f.read()
with contextlib.closing(
tar.extractfile(
tar.getmember(
start + "pybind11/share/cmake/pybind11/pybind11Config.cmake"
) )
assert (
'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")'
in cmake_cfg.decode("utf-8")
) )
) as f:
contents = f.read().decode("utf8")
assert 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")' in contents
files = {f"pybind11/{n}" for n in all_files} files = {f"pybind11/{n}" for n in all_files}
files |= sdist_files files |= sdist_files
@ -154,9 +164,9 @@ def test_build_sdist(monkeypatch, tmpdir):
with open(os.path.join(MAIN_DIR, "tools", "setup_main.py.in"), "rb") as f: with open(os.path.join(MAIN_DIR, "tools", "setup_main.py.in"), "rb") as f:
contents = ( contents = (
string.Template(f.read().decode()) string.Template(f.read().decode("utf-8"))
.substitute(version=version, extra_cmd="") .substitute(version=version, extra_cmd="")
.encode() .encode("utf-8")
) )
assert setup_py == contents assert setup_py == contents
@ -164,25 +174,19 @@ def test_build_sdist(monkeypatch, tmpdir):
contents = f.read() contents = f.read()
assert pyproject_toml == contents assert pyproject_toml == contents
simple_version = ".".join(version.split(".")[:3])
pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version).encode("utf-8")
assert normalize_line_endings(pkgconfig) == pkgconfig_expected
def test_build_global_dist(monkeypatch, tmpdir): def test_build_global_dist(monkeypatch, tmpdir):
monkeypatch.chdir(MAIN_DIR) monkeypatch.chdir(MAIN_DIR)
monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1") monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1")
out = subprocess.check_output( subprocess.run(
[ [sys.executable, "-m", "build", "--sdist", "--outdir", str(tmpdir)], check=True
sys.executable,
"-m",
"build",
"--sdist",
"--outdir",
str(tmpdir),
]
) )
if hasattr(out, "decode"):
out = out.decode()
(sdist,) = tmpdir.visit("*.tar.gz") (sdist,) = tmpdir.visit("*.tar.gz")
with tarfile.open(str(sdist), "r:gz") as tar: with tarfile.open(str(sdist), "r:gz") as tar:
@ -190,15 +194,17 @@ def test_build_global_dist(monkeypatch, tmpdir):
version = start[16:-1] version = start[16:-1]
simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]} simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]}
with contextlib.closing( setup_py = read_tz_file(tar, "setup.py")
tar.extractfile(tar.getmember(start + "setup.py")) pyproject_toml = read_tz_file(tar, "pyproject.toml")
) as f: pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc")
setup_py = f.read() cmake_cfg = read_tz_file(
tar, "pybind11/share/cmake/pybind11/pybind11Config.cmake"
)
with contextlib.closing( assert (
tar.extractfile(tar.getmember(start + "pyproject.toml")) 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")'
) as f: in cmake_cfg.decode("utf-8")
pyproject_toml = f.read() )
files = {f"pybind11/{n}" for n in all_files} files = {f"pybind11/{n}" for n in all_files}
files |= sdist_files files |= sdist_files
@ -209,7 +215,7 @@ def test_build_global_dist(monkeypatch, tmpdir):
contents = ( contents = (
string.Template(f.read().decode()) string.Template(f.read().decode())
.substitute(version=version, extra_cmd="") .substitute(version=version, extra_cmd="")
.encode() .encode("utf-8")
) )
assert setup_py == contents assert setup_py == contents
@ -217,12 +223,16 @@ def test_build_global_dist(monkeypatch, tmpdir):
contents = f.read() contents = f.read()
assert pyproject_toml == contents assert pyproject_toml == contents
simple_version = ".".join(version.split(".")[:3])
pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version).encode("utf-8")
assert normalize_line_endings(pkgconfig) == pkgconfig_expected
def tests_build_wheel(monkeypatch, tmpdir): def tests_build_wheel(monkeypatch, tmpdir):
monkeypatch.chdir(MAIN_DIR) monkeypatch.chdir(MAIN_DIR)
subprocess.check_output( subprocess.run(
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)] [sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)], check=True
) )
(wheel,) = tmpdir.visit("*.whl") (wheel,) = tmpdir.visit("*.whl")
@ -249,8 +259,8 @@ def tests_build_global_wheel(monkeypatch, tmpdir):
monkeypatch.chdir(MAIN_DIR) monkeypatch.chdir(MAIN_DIR)
monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1") monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1")
subprocess.check_output( subprocess.run(
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)] [sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)], check=True
) )
(wheel,) = tmpdir.visit("*.whl") (wheel,) = tmpdir.visit("*.whl")

23
tools/JoinPaths.cmake Normal file
View File

@ -0,0 +1,23 @@
# This module provides function for joining paths
# known from most languages
#
# SPDX-License-Identifier: (MIT OR CC0-1.0)
# Copyright 2020 Jan Tojnar
# https://github.com/jtojnar/cmake-snips
#
# Modelled after Pythons os.path.join
# https://docs.python.org/3.7/library/os.path.html#os.path.join
# Windows not supported
function(join_paths joined_path first_path_segment)
set(temp_path "${first_path_segment}")
foreach(current_segment IN LISTS ARGN)
if(NOT ("${current_segment}" STREQUAL ""))
if(IS_ABSOLUTE "${current_segment}")
set(temp_path "${current_segment}")
else()
set(temp_path "${temp_path}/${current_segment}")
endif()
endif()
endforeach()
set(${joined_path} "${temp_path}" PARENT_SCOPE)
endfunction()

7
tools/pybind11.pc.in Normal file
View File

@ -0,0 +1,7 @@
prefix=@prefix_for_pc_file@
includedir=@includedir_for_pc_file@
Name: @PROJECT_NAME@
Description: Seamless operability between C++11 and Python
Version: @PROJECT_VERSION@
Cflags: -I${includedir}

View File

@ -29,6 +29,7 @@ main_headers = glob.glob("pybind11/include/pybind11/*.h")
detail_headers = glob.glob("pybind11/include/pybind11/detail/*.h") detail_headers = glob.glob("pybind11/include/pybind11/detail/*.h")
stl_headers = glob.glob("pybind11/include/pybind11/stl/*.h") stl_headers = glob.glob("pybind11/include/pybind11/stl/*.h")
cmake_files = glob.glob("pybind11/share/cmake/pybind11/*.cmake") cmake_files = glob.glob("pybind11/share/cmake/pybind11/*.cmake")
pkgconfig_files = glob.glob("pybind11/share/pkgconfig/*.pc")
headers = main_headers + detail_headers + stl_headers headers = main_headers + detail_headers + stl_headers
cmdclass = {"install_headers": InstallHeadersNested} cmdclass = {"install_headers": InstallHeadersNested}
@ -51,6 +52,7 @@ setup(
headers=headers, headers=headers,
data_files=[ data_files=[
(base + "share/cmake/pybind11", cmake_files), (base + "share/cmake/pybind11", cmake_files),
(base + "share/pkgconfig", pkgconfig_files),
(base + "include/pybind11", main_headers), (base + "include/pybind11", main_headers),
(base + "include/pybind11/detail", detail_headers), (base + "include/pybind11/detail", detail_headers),
(base + "include/pybind11/stl", stl_headers), (base + "include/pybind11/stl", stl_headers),

View File

@ -17,6 +17,7 @@ setup(
"pybind11.include.pybind11.detail", "pybind11.include.pybind11.detail",
"pybind11.include.pybind11.stl", "pybind11.include.pybind11.stl",
"pybind11.share.cmake.pybind11", "pybind11.share.cmake.pybind11",
"pybind11.share.pkgconfig",
], ],
package_data={ package_data={
"pybind11": ["py.typed"], "pybind11": ["py.typed"],
@ -24,6 +25,7 @@ setup(
"pybind11.include.pybind11.detail": ["*.h"], "pybind11.include.pybind11.detail": ["*.h"],
"pybind11.include.pybind11.stl": ["*.h"], "pybind11.include.pybind11.stl": ["*.h"],
"pybind11.share.cmake.pybind11": ["*.cmake"], "pybind11.share.cmake.pybind11": ["*.cmake"],
"pybind11.share.pkgconfig": ["*.pc"],
}, },
extras_require={ extras_require={
"global": ["pybind11_global==$version"] "global": ["pybind11_global==$version"]