Skip to content

add installation support for pkg-config dependency detection #4077

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 9, 2022
Merged
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
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