Skip to content

Commit

Permalink
feat(cmake): add installation support for pkg-config dependency detec…
Browse files Browse the repository at this point in the history
…tion (#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 <[email protected]>

Co-authored-by: Henry Schreiner <[email protected]>
  • Loading branch information
eli-schwartz and henryiii authored Aug 9, 2022
1 parent 14c8465 commit 5bdd3d5
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 62 deletions.
3 changes: 3 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#
# See https://github.com/pre-commit/pre-commit

# third-party content
exclude: ^tools/JoinPaths.cmake$

repos:
# Standard hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
13 changes: 13 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ else()
endif()

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
if(USE_PYTHON_INCLUDE_DIR AND DEFINED Python_INCLUDE_DIRS)
Expand Down Expand Up @@ -262,6 +265,16 @@ if(PYBIND11_INSTALL)
NAMESPACE "pybind11::"
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
if(PYBIND11_MASTER_PROJECT)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake_uninstall.cmake.in"
Expand Down
4 changes: 2 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def lint(session: nox.Session) -> None:
Lint the codebase (except for clang-format/tidy).
"""
session.install("pre-commit")
session.run("pre-commit", "run", "-a")
session.run("pre-commit", "run", "-a", *session.posargs)


@nox.session(python=PYTHON_VERSIONS)
Expand Down Expand Up @@ -58,7 +58,7 @@ def tests_packaging(session: nox.Session) -> None:
"""

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)
Expand Down
3 changes: 2 additions & 1 deletion pybind11/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@


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__ = (
"version_info",
"__version__",
"get_include",
"get_cmake_dir",
"get_pkgconfig_dir",
)
9 changes: 8 additions & 1 deletion pybind11/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import sys
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:
Expand Down Expand Up @@ -36,13 +36,20 @@ def main() -> None:
action="store_true",
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()
if not sys.argv[1:]:
parser.print_help()
if args.includes:
print_includes()
if args.cmakedir:
print(get_cmake_dir())
if args.pkgconfigdir:
print(get_pkgconfig_dir())


if __name__ == "__main__":
Expand Down
12 changes: 12 additions & 0 deletions pybind11/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ def get_cmake_dir() -> str:

msg = "pybind11 not installed, installation required to access the CMake files"
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)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def remove_output(*sources: str) -> Iterator[None]:
"-DCMAKE_INSTALL_PREFIX=pybind11",
"-DBUILD_TESTING=OFF",
"-DPYBIND11_NOPYTHON=ON",
"-Dprefix_for_pc_file=${pcfiledir}/../../",
]
if "CMAKE_ARGS" in os.environ:
fcommand = [
Expand Down
126 changes: 68 additions & 58 deletions tests/extra_python_package/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
DIR = os.path.abspath(os.path.dirname(__file__))
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 = {
"include/pybind11/attr.h",
Expand Down Expand Up @@ -59,6 +69,10 @@
"share/cmake/pybind11/pybind11Tools.cmake",
}

pkgconfig_files = {
"share/pkgconfig/pybind11.pc",
}

py_files = {
"__init__.py",
"__main__.py",
Expand All @@ -69,7 +83,7 @@
}

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


Expand All @@ -82,6 +96,7 @@
"pybind11/share",
"pybind11/share/cmake",
"pybind11/share/cmake/pybind11",
"pybind11/share/pkgconfig",
"pyproject.toml",
"setup.cfg",
"setup.py",
Expand All @@ -101,22 +116,25 @@
}


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):

monkeypatch.chdir(MAIN_DIR)

out = subprocess.check_output(
[
sys.executable,
"-m",
"build",
"--sdist",
"--outdir",
str(tmpdir),
]
subprocess.run(
[sys.executable, "-m", "build", "--sdist", f"--outdir={tmpdir}"], check=True
)
if hasattr(out, "decode"):
out = out.decode()

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

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

with contextlib.closing(
tar.extractfile(tar.getmember(start + "setup.py"))
) as f:
setup_py = f.read()

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"
)
)
) as f:
contents = f.read().decode("utf8")
assert 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")' in contents
setup_py = read_tz_file(tar, "setup.py")
pyproject_toml = read_tz_file(tar, "pyproject.toml")
pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc")
cmake_cfg = read_tz_file(
tar, "pybind11/share/cmake/pybind11/pybind11Config.cmake"
)

assert (
'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")'
in cmake_cfg.decode("utf-8")
)

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

with open(os.path.join(MAIN_DIR, "tools", "setup_main.py.in"), "rb") as f:
contents = (
string.Template(f.read().decode())
string.Template(f.read().decode("utf-8"))
.substitute(version=version, extra_cmd="")
.encode()
.encode("utf-8")
)
assert setup_py == contents

with open(os.path.join(MAIN_DIR, "tools", "pyproject.toml"), "rb") as f:
contents = f.read()
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):

monkeypatch.chdir(MAIN_DIR)
monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1")
out = subprocess.check_output(
[
sys.executable,
"-m",
"build",
"--sdist",
"--outdir",
str(tmpdir),
]
subprocess.run(
[sys.executable, "-m", "build", "--sdist", "--outdir", str(tmpdir)], check=True
)

if hasattr(out, "decode"):
out = out.decode()

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

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

with contextlib.closing(
tar.extractfile(tar.getmember(start + "setup.py"))
) as f:
setup_py = f.read()
setup_py = read_tz_file(tar, "setup.py")
pyproject_toml = read_tz_file(tar, "pyproject.toml")
pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc")
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()
assert (
'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")'
in cmake_cfg.decode("utf-8")
)

files = {f"pybind11/{n}" for n in all_files}
files |= sdist_files
Expand All @@ -209,20 +215,24 @@ def test_build_global_dist(monkeypatch, tmpdir):
contents = (
string.Template(f.read().decode())
.substitute(version=version, extra_cmd="")
.encode()
.encode("utf-8")
)
assert setup_py == contents

with open(os.path.join(MAIN_DIR, "tools", "pyproject.toml"), "rb") as f:
contents = f.read()
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):
monkeypatch.chdir(MAIN_DIR)

subprocess.check_output(
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)]
subprocess.run(
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)], check=True
)

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

subprocess.check_output(
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)]
subprocess.run(
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)], check=True
)

(wheel,) = tmpdir.visit("*.whl")
Expand Down
23 changes: 23 additions & 0 deletions tools/JoinPaths.cmake
Original file line number Diff line number Diff line change
@@ -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 Python’s 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 changes: 7 additions & 0 deletions tools/pybind11.pc.in
Original file line number Diff line number Diff line change
@@ -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}
2 changes: 2 additions & 0 deletions tools/setup_global.py.in
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ main_headers = glob.glob("pybind11/include/pybind11/*.h")
detail_headers = glob.glob("pybind11/include/pybind11/detail/*.h")
stl_headers = glob.glob("pybind11/include/pybind11/stl/*.h")
cmake_files = glob.glob("pybind11/share/cmake/pybind11/*.cmake")
pkgconfig_files = glob.glob("pybind11/share/pkgconfig/*.pc")
headers = main_headers + detail_headers + stl_headers

cmdclass = {"install_headers": InstallHeadersNested}
Expand All @@ -51,6 +52,7 @@ setup(
headers=headers,
data_files=[
(base + "share/cmake/pybind11", cmake_files),
(base + "share/pkgconfig", pkgconfig_files),
(base + "include/pybind11", main_headers),
(base + "include/pybind11/detail", detail_headers),
(base + "include/pybind11/stl", stl_headers),
Expand Down
Loading

0 comments on commit 5bdd3d5

Please sign in to comment.