From f4afa9051b34c769bd18819a7f96a74ea802503b Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 22 Jul 2026 19:47:30 -0400 Subject: [PATCH 1/4] feat: small command line helpers for quick tests Add --cflags, --ldflags, --embed, and --file to the pybind11 command line tool, based on python-config. This makes quick one-off compiles easy: c++ $(python3 -m pybind11 --file=example.cpp) Assisted-by: ClaudeCode:claude-fable-5 --- docs/compiling.rst | 15 ++++++ pybind11/__main__.py | 54 +++++++++++++++------ pybind11/commands.py | 61 ++++++++++++++++++++++++ tests/extra_python_package/test_files.py | 40 ++++++++++++++++ 4 files changed, 155 insertions(+), 15 deletions(-) diff --git a/docs/compiling.rst b/docs/compiling.rst index b693bd5878..8e85c0bfed 100644 --- a/docs/compiling.rst +++ b/docs/compiling.rst @@ -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 diff --git a/pybind11/__main__.py b/pybind11/__main__.py index 21c3cd9abc..20bc1e587a 100644 --- a/pybind11/__main__.py +++ b/pybind11/__main__.py @@ -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: @@ -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: @@ -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: diff --git a/pybind11/commands.py b/pybind11/commands.py index d535b6cca4..2a9f6b4244 100644 --- a/pybind11/commands.py +++ b/pybind11/commands.py @@ -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" @@ -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) diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index d96e9afc1f..e17000a756 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -6,6 +6,7 @@ import shutil import subprocess import sys +import sysconfig import tarfile import zipfile from pathlib import Path @@ -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") From 66c4b8daa0e48b9a9127d97b70519ae8cff0a184 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 00:21:11 -0400 Subject: [PATCH 2/4] refactor: quote paths, clarify CLI output logic, strengthen tests - Quote include and library dirs in get_cflags/get_ldflags so paths with spaces work, sharing one quote helper in commands.py - Restructure the --file/--cflags/--ldflags printing into a single print - Note the Unix-compiler orientation in help text and docs - Test -L presence for --embed and quoting of paths with spaces Assisted-by: ClaudeCode:claude-fable-5 --- docs/compiling.rst | 4 ++ pybind11/__main__.py | 57 +++++++++--------------- pybind11/commands.py | 47 +++++++++++++++---- tests/extra_python_package/test_files.py | 16 +++++++ 4 files changed, 81 insertions(+), 43 deletions(-) diff --git a/docs/compiling.rst b/docs/compiling.rst index 8e85c0bfed..1838777e63 100644 --- a/docs/compiling.rst +++ b/docs/compiling.rst @@ -680,6 +680,10 @@ embeds the interpreter instead of an extension): $ c++ $(python3 -m pybind11 --file=example.cpp) +These helpers target Unix-style compilers (GCC/Clang) and are intended for +quick tests, not production builds; ``--file`` places the output next to the +source file. + 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 diff --git a/pybind11/__main__.py b/pybind11/__main__.py index 20bc1e587a..53b164ce97 100644 --- a/pybind11/__main__.py +++ b/pybind11/__main__.py @@ -3,12 +3,14 @@ import argparse import functools -import re import sys import sysconfig from pathlib import Path from ._version import __version__ +from .commands import ( + _quote as quote, +) from .commands import ( get_cflags, get_cmake_dir, @@ -17,28 +19,6 @@ get_pkgconfig_dir, ) -# This is the conditional used for os.path being posixpath -if "posix" in sys.builtin_module_names: - from shlex import quote -elif "nt" in sys.builtin_module_names: - # See https://github.com/mesonbuild/meson/blob/db22551ed9d2dd7889abea01cc1c7bba02bf1c75/mesonbuild/utils/universal.py#L1092-L1121 - # and the original documents: - # https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments and - # https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ - UNSAFE = re.compile("[ \t\n\r]") - - def quote(s: str) -> str: - if s and not UNSAFE.search(s): - return s - - # Paths cannot contain a '"' on Windows, so we don't need to worry - # about nuanced counting here. - return f'"{s}\\"' if s.endswith("\\") else f'"{s}"' -else: - - def quote(s: str) -> str: - return s - def print_includes() -> None: print(" ".join(quote(f"-I{d}") for d in get_include_dirs())) @@ -78,12 +58,12 @@ def main() -> None: parser.add_argument( "--cflags", action="store_true", - help="Print the compile flags for a simple extension.", + help="Print the compile flags for a simple extension (Unix-style compilers).", ) parser.add_argument( "--ldflags", action="store_true", - help="Print the link flags for a simple extension.", + help="Print the link flags for a simple extension (Unix-style compilers).", ) parser.add_argument( "--embed", @@ -93,22 +73,29 @@ def main() -> None: parser.add_argument( "--file", type=Path, - help="Print a full command-line suffix for compiling the given file.", + help="Print a full command-line suffix for compiling the given file;" + " the output goes next to the source file (Unix-style compilers).", ) args = parser.parse_args() if not sys.argv[1:]: parser.print_help() - 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)))) + print( + get_cflags(), + quote(str(args.file)), + get_ldflags(embed=args.embed), + "-o", + quote(str(args.file.with_suffix(suffix))), + ) + else: + if args.cflags: + print(get_cflags()) + if args.ldflags: + print(get_ldflags(embed=args.embed)) + # --cflags and --file already contain the include flags + if args.includes and not (args.cflags or args.file): + print_includes() if args.cmakedir: print(quote(get_cmake_dir())) if args.pkgconfigdir: diff --git a/pybind11/commands.py b/pybind11/commands.py index 2a9f6b4244..b13df5f67e 100644 --- a/pybind11/commands.py +++ b/pybind11/commands.py @@ -1,15 +1,44 @@ from __future__ import annotations import os +import re import sys import sysconfig DIR = os.path.abspath(os.path.dirname(__file__)) +# This is the conditional used for os.path being posixpath +if "posix" in sys.builtin_module_names: + import shlex -def _get_config_var(name: str, fmt: str = "{}") -> list[str]: + def _quote(s: str) -> str: + return shlex.quote(s) +elif "nt" in sys.builtin_module_names: + # See https://github.com/mesonbuild/meson/blob/db22551ed9d2dd7889abea01cc1c7bba02bf1c75/mesonbuild/utils/universal.py#L1092-L1121 + # and the original documents: + # https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments and + # https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ + _UNSAFE = re.compile("[ \t\n\r]") + + def _quote(s: str) -> str: + if s and not _UNSAFE.search(s): + return s + + # Paths cannot contain a '"' on Windows, so we don't need to worry + # about nuanced counting here. + return f'"{s}\\"' if s.endswith("\\") else f'"{s}"' +else: + + def _quote(s: str) -> str: + return s + + +def _get_config_var(name: str, fmt: str = "{}", quote: bool = False) -> list[str]: var = sysconfig.get_config_var(name) - return [fmt.format(str(var).strip())] if var else [] + if not var: + return [] + flag = fmt.format(str(var).strip()) + return [_quote(flag) if quote else flag] def get_include(user: bool = False) -> str: # noqa: ARG001 @@ -67,9 +96,11 @@ def get_include_dirs() -> list[str]: def get_cflags() -> str: """ Return the compile flags for building a simple extension with a - command-line compiler. Based on python-config. + Unix-style command-line compiler (GCC/Clang). Based on python-config. """ - flags = [f"-I{d}" for d in get_include_dirs()] + flags = [_quote(f"-I{d}") for d in get_include_dirs()] + # CFLAGS/LDFLAGS/LIBS/SYSLIBS are pre-composed multi-flag strings, passed + # through as-is (like python-config does) flags += _get_config_var("CFLAGS") flags.append("-std=c++17") return " ".join(flags) @@ -78,15 +109,15 @@ def get_cflags() -> str: 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. + embed=True, an embedding program) with a Unix-style command-line + compiler (GCC/Clang). Based on python-config. """ flags = _get_config_var("LDFLAGS") if embed: - flags += _get_config_var("LIBDIR", "-L{}") + flags += _get_config_var("LIBDIR", "-L{}", quote=True) if not sysconfig.get_config_var("Py_ENABLE_SHARED"): - flags += _get_config_var("LIBPL", "-L{}") + flags += _get_config_var("LIBPL", "-L{}", quote=True) version = sysconfig.get_config_var("VERSION") or "" abiflags = getattr(sys, "abiflags", "") or "" flags.append(f"-lpython{version}{abiflags}") diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index e17000a756..c8cf9aa597 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -1,6 +1,7 @@ from __future__ import annotations import contextlib +import importlib.util import os import re import shutil @@ -12,6 +13,8 @@ from pathlib import Path from typing import Generator +import pytest + # These tests must be run explicitly DIR = Path(__file__).parent.resolve() @@ -409,6 +412,19 @@ def test_cli_cflags(): def test_cli_ldflags_embed(): out = run_command_line("--ldflags", "--embed") assert "-lpython" in out + if sysconfig.get_config_var("LIBDIR"): + assert "-L" in out + + +@pytest.mark.skipif(os.name != "posix", reason="quote style is platform-specific") +def test_cflags_quotes_paths_with_spaces(monkeypatch): + spec = importlib.util.spec_from_file_location( + "pybind11_commands", MAIN_DIR / "pybind11" / "commands.py" + ) + commands = importlib.util.module_from_spec(spec) + spec.loader.exec_module(commands) + monkeypatch.setattr(commands.sysconfig, "get_path", lambda name: f"/spa ced/{name}") + assert "'-I/spa ced/include'" in commands.get_cflags() def test_cli_file(): From 5eef4faf681e8ba6aa13e16c3cc7a7e4c1d3fcc0 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 00:27:33 -0400 Subject: [PATCH 3/4] refactor: simplify flag helpers and keep import pybind11 light - Replace _get_config_var(name, fmt, quote) with a plain _config(name) string helper; formatting and quoting happen at the call sites - Defer sysconfig/shlex imports so import pybind11 does not pay for the CLI (~1ms -> ~0.08ms for pybind11.commands) - Fetch EXT_SUFFIX once in main() - Import commands normally in the quoting test instead of importlib file loading Assisted-by: ClaudeCode:claude-fable-5 --- pybind11/__main__.py | 5 +-- pybind11/commands.py | 46 ++++++++++++++---------- tests/extra_python_package/test_files.py | 11 +++--- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/pybind11/__main__.py b/pybind11/__main__.py index 53b164ce97..ce597c781a 100644 --- a/pybind11/__main__.py +++ b/pybind11/__main__.py @@ -79,8 +79,9 @@ def main() -> None: args = parser.parse_args() if not sys.argv[1:]: parser.print_help() + ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") or "" if args.file: - suffix = "" if args.embed else sysconfig.get_config_var("EXT_SUFFIX") or "" + suffix = "" if args.embed else ext_suffix print( get_cflags(), quote(str(args.file)), @@ -101,7 +102,7 @@ def main() -> None: if args.pkgconfigdir: print(quote(get_pkgconfig_dir())) if args.extension_suffix: - print(sysconfig.get_config_var("EXT_SUFFIX")) + print(ext_suffix) if __name__ == "__main__": diff --git a/pybind11/commands.py b/pybind11/commands.py index b13df5f67e..7d7928e5b0 100644 --- a/pybind11/commands.py +++ b/pybind11/commands.py @@ -3,15 +3,17 @@ import os import re import sys -import sysconfig DIR = os.path.abspath(os.path.dirname(__file__)) +# shlex/sysconfig are imported lazily below to keep `import pybind11` light + # This is the conditional used for os.path being posixpath if "posix" in sys.builtin_module_names: - import shlex def _quote(s: str) -> str: + import shlex + return shlex.quote(s) elif "nt" in sys.builtin_module_names: # See https://github.com/mesonbuild/meson/blob/db22551ed9d2dd7889abea01cc1c7bba02bf1c75/mesonbuild/utils/universal.py#L1092-L1121 @@ -33,12 +35,11 @@ def _quote(s: str) -> str: return s -def _get_config_var(name: str, fmt: str = "{}", quote: bool = False) -> list[str]: - var = sysconfig.get_config_var(name) - if not var: - return [] - flag = fmt.format(str(var).strip()) - return [_quote(flag) if quote else flag] +def _config(name: str) -> str: + """A stripped sysconfig variable, or "" if unset.""" + import sysconfig + + return str(sysconfig.get_config_var(name) or "").strip() def get_include(user: bool = False) -> str: # noqa: ARG001 @@ -79,6 +80,8 @@ def get_include_dirs() -> list[str]: """ Return the unique include directories for Python and pybind11. """ + import sysconfig + dirs = [ sysconfig.get_path("include"), sysconfig.get_path("platinclude"), @@ -99,9 +102,10 @@ def get_cflags() -> str: Unix-style command-line compiler (GCC/Clang). Based on python-config. """ flags = [_quote(f"-I{d}") for d in get_include_dirs()] - # CFLAGS/LDFLAGS/LIBS/SYSLIBS are pre-composed multi-flag strings, passed - # through as-is (like python-config does) - flags += _get_config_var("CFLAGS") + # CFLAGS is a pre-composed multi-flag string, passed through as-is (like + # python-config does) + if cflags := _config("CFLAGS"): + flags.append(cflags) flags.append("-std=c++17") return " ".join(flags) @@ -112,17 +116,21 @@ def get_ldflags(embed: bool = False) -> str: embed=True, an embedding program) with a Unix-style command-line compiler (GCC/Clang). Based on python-config. """ - flags = _get_config_var("LDFLAGS") + # LDFLAGS/LIBS/SYSLIBS are pre-composed multi-flag strings, passed + # through as-is (like python-config does) + flags = [ldflags] if (ldflags := _config("LDFLAGS")) else [] if embed: - flags += _get_config_var("LIBDIR", "-L{}", quote=True) - if not sysconfig.get_config_var("Py_ENABLE_SHARED"): - flags += _get_config_var("LIBPL", "-L{}", quote=True) - version = sysconfig.get_config_var("VERSION") or "" + if libdir := _config("LIBDIR"): + flags.append(_quote(f"-L{libdir}")) + if not _config("Py_ENABLE_SHARED") and (libpl := _config("LIBPL")): + flags.append(_quote(f"-L{libpl}")) abiflags = getattr(sys, "abiflags", "") or "" - flags.append(f"-lpython{version}{abiflags}") - flags += _get_config_var("LIBS") - flags += _get_config_var("SYSLIBS") + flags.append(f"-lpython{_config('VERSION')}{abiflags}") + if libs := _config("LIBS"): + flags.append(libs) + if syslibs := _config("SYSLIBS"): + flags.append(syslibs) elif sys.platform.startswith("darwin"): flags += ["-undefined", "dynamic_lookup", "-shared"] elif sys.platform.startswith("linux"): diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index c8cf9aa597..a9caf2e828 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -1,7 +1,7 @@ from __future__ import annotations import contextlib -import importlib.util +import importlib import os import re import shutil @@ -418,12 +418,9 @@ def test_cli_ldflags_embed(): @pytest.mark.skipif(os.name != "posix", reason="quote style is platform-specific") def test_cflags_quotes_paths_with_spaces(monkeypatch): - spec = importlib.util.spec_from_file_location( - "pybind11_commands", MAIN_DIR / "pybind11" / "commands.py" - ) - commands = importlib.util.module_from_spec(spec) - spec.loader.exec_module(commands) - monkeypatch.setattr(commands.sysconfig, "get_path", lambda name: f"/spa ced/{name}") + monkeypatch.syspath_prepend(str(MAIN_DIR)) + commands = importlib.import_module("pybind11.commands") + monkeypatch.setattr(sysconfig, "get_path", lambda name: f"/spa ced/{name}") assert "'-I/spa ced/include'" in commands.get_cflags() From 4b5a04add1b3ddbc08c1338bae07409ec2dda7d7 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 00:34:03 -0400 Subject: [PATCH 4/4] fix: shared-library flags on all Unix platforms The --file/--ldflags output only added -shared and -fPIC on Linux, so other Unix platforms (FreeBSD, Solaris, AIX) linked an executable and failed on the missing main. Gate on os.name instead. Assisted-by: ClaudeCode:claude-opus-5 --- pybind11/commands.py | 3 ++- tests/extra_python_package/test_files.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pybind11/commands.py b/pybind11/commands.py index 7d7928e5b0..8bd0a9bf13 100644 --- a/pybind11/commands.py +++ b/pybind11/commands.py @@ -133,7 +133,8 @@ def get_ldflags(embed: bool = False) -> str: flags.append(syslibs) elif sys.platform.startswith("darwin"): flags += ["-undefined", "dynamic_lookup", "-shared"] - elif sys.platform.startswith("linux"): + elif os.name == "posix": + # Linux and other Unix platforms (FreeBSD, Solaris, AIX, ...) flags += ["-fPIC", "-shared"] return " ".join(flags) diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index a9caf2e828..0abb48c1f4 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -424,6 +424,17 @@ def test_cflags_quotes_paths_with_spaces(monkeypatch): assert "'-I/spa ced/include'" in commands.get_cflags() +@pytest.mark.skipif(os.name != "posix", reason="Unix link flags only") +def test_ldflags_other_unix(monkeypatch): + monkeypatch.syspath_prepend(str(MAIN_DIR)) + commands = importlib.import_module("pybind11.commands") + monkeypatch.setattr(sys, "platform", "freebsd14") + monkeypatch.setattr(commands, "_config", lambda name: "") # noqa: ARG005 + out = commands.get_ldflags() + assert "-shared" in out + assert "-fPIC" in out + + def test_cli_file(): out = run_command_line("--file", "example.cpp").rstrip() ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")