Skip to content
Open
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
15 changes: 15 additions & 0 deletions docs/compiling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,21 @@ building the module:

$ c++ -O3 -Wall -shared -std=c++11 -undefined dynamic_lookup $(python3 -m pybind11 --includes) example.cpp -o example$(python3-config --extension-suffix)

For quick tests, the command line tool can also produce the full set of flags
for you, based on ``python-config``:

.. code-block:: bash

$ c++ $(python3 -m pybind11 --cflags) example.cpp $(python3 -m pybind11 --ldflags) -o example$(python3 -m pybind11 --extension-suffix)

Or, shorter still, ``--file`` prints everything after the compiler for a given
source file, including the output name (add ``--embed`` for a program that
embeds the interpreter instead of an extension):

.. code-block:: bash

$ c++ $(python3 -m pybind11 --file=example.cpp)

In general, it is advisable to include several additional build parameters
that can considerably reduce the size of the created binary. Refer to section
:ref:`cmake` for a detailed example of a suitable cross-platform CMake-based
Expand Down
54 changes: 39 additions & 15 deletions pybind11/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@
import re
import sys
import sysconfig
from pathlib import Path

from ._version import __version__
from .commands import get_cmake_dir, get_include, get_pkgconfig_dir
from .commands import (
get_cflags,
get_cmake_dir,
get_include_dirs,
get_ldflags,
get_pkgconfig_dir,
)

# This is the conditional used for os.path being posixpath
if "posix" in sys.builtin_module_names:
Expand All @@ -34,19 +41,7 @@ def quote(s: str) -> str:


def print_includes() -> None:
dirs = [
sysconfig.get_path("include"),
sysconfig.get_path("platinclude"),
get_include(),
]

# Make unique but preserve order
unique_dirs = []
for d in dirs:
if d and d not in unique_dirs:
unique_dirs.append(d)

print(" ".join(quote(f"-I{d}") for d in unique_dirs))
print(" ".join(quote(f"-I{d}") for d in get_include_dirs()))


def main() -> None:
Expand Down Expand Up @@ -80,11 +75,40 @@ def main() -> None:
action="store_true",
help="Print the extension for a Python module",
)
parser.add_argument(
"--cflags",
action="store_true",
help="Print the compile flags for a simple extension.",
)
parser.add_argument(
"--ldflags",
action="store_true",
help="Print the link flags for a simple extension.",
)
parser.add_argument(
"--embed",
action="store_true",
help="Build for embedding instead of an extension; affects --ldflags and --file.",
)
parser.add_argument(
"--file",
type=Path,
help="Print a full command-line suffix for compiling the given file.",
)
args = parser.parse_args()
if not sys.argv[1:]:
parser.print_help()
if args.includes:
if args.cflags or args.file:
print(get_cflags(), end=" " if args.file else "\n")
elif args.includes:
print_includes()
if args.file:
print(quote(str(args.file)), end=" ")
if args.ldflags or args.file:
print(get_ldflags(embed=args.embed), end=" " if args.file else "\n")
if args.file:
suffix = "" if args.embed else sysconfig.get_config_var("EXT_SUFFIX") or ""
print("-o", quote(str(args.file.with_suffix(suffix))))
if args.cmakedir:
print(quote(get_cmake_dir()))
if args.pkgconfigdir:
Expand Down
61 changes: 61 additions & 0 deletions pybind11/commands.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from __future__ import annotations

import os
import sys
import sysconfig

DIR = os.path.abspath(os.path.dirname(__file__))


def _get_config_var(name: str, fmt: str = "{}") -> list[str]:
var = sysconfig.get_config_var(name)
return [fmt.format(str(var).strip())] if var else []


def get_include(user: bool = False) -> str: # noqa: ARG001
"""
Return the path to the pybind11 include directory. The historical "user"
Expand Down Expand Up @@ -37,3 +44,57 @@ def get_pkgconfig_dir() -> str:

msg = "pybind11 not installed, installation required to access the pkgconfig files"
raise ImportError(msg)


def get_include_dirs() -> list[str]:
"""
Return the unique include directories for Python and pybind11.
"""
dirs = [
sysconfig.get_path("include"),
sysconfig.get_path("platinclude"),
get_include(),
]

# Make unique but preserve order
unique_dirs = []
for d in dirs:
if d and d not in unique_dirs:
unique_dirs.append(d)
return unique_dirs


def get_cflags() -> str:
"""
Return the compile flags for building a simple extension with a
command-line compiler. Based on python-config.
"""
flags = [f"-I{d}" for d in get_include_dirs()]
flags += _get_config_var("CFLAGS")
flags.append("-std=c++17")
return " ".join(flags)


def get_ldflags(embed: bool = False) -> str:
"""
Return the link flags for building a simple extension (or, with
embed=True, an embedding program) with a command-line compiler.
Based on python-config.
"""
flags = _get_config_var("LDFLAGS")

if embed:
flags += _get_config_var("LIBDIR", "-L{}")
if not sysconfig.get_config_var("Py_ENABLE_SHARED"):
flags += _get_config_var("LIBPL", "-L{}")
version = sysconfig.get_config_var("VERSION") or ""
abiflags = getattr(sys, "abiflags", "") or ""
flags.append(f"-lpython{version}{abiflags}")
flags += _get_config_var("LIBS")
flags += _get_config_var("SYSLIBS")
elif sys.platform.startswith("darwin"):
flags += ["-undefined", "dynamic_lookup", "-shared"]
elif sys.platform.startswith("linux"):
flags += ["-fPIC", "-shared"]

return " ".join(flags)
40 changes: 40 additions & 0 deletions tests/extra_python_package/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import shutil
import subprocess
import sys
import sysconfig
import tarfile
import zipfile
from pathlib import Path
Expand Down Expand Up @@ -384,3 +385,42 @@ def test_version_matches():
expected_patch = f"{micro}{level_str}{release_serial}"

assert patch == expected_patch


def run_command_line(*args: str) -> str:
env = os.environ.copy()
env["PYTHONPATH"] = str(MAIN_DIR)
result = subprocess.run(
[sys.executable, "-m", "pybind11", *args],
capture_output=True,
text=True,
check=True,
env=env,
)
return result.stdout


def test_cli_cflags():
out = run_command_line("--cflags")
assert "-std=c++17" in out
assert f"-I{sysconfig.get_path('include')}" in out


def test_cli_ldflags_embed():
out = run_command_line("--ldflags", "--embed")
assert "-lpython" in out


def test_cli_file():
out = run_command_line("--file", "example.cpp").rstrip()
ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")
assert "-std=c++17" in out
assert out.index("-std=c++17") < out.index("example.cpp")
assert out.endswith(f"-o example{ext_suffix}")
if sys.platform.startswith(("linux", "darwin")):
assert out.index("example.cpp") < out.index("-shared")


def test_cli_file_embed():
out = run_command_line("--file", "example.cpp", "--embed").rstrip()
assert out.endswith("-o example")
Loading