Skip to content

feat: small command line helpers for quick tests#4272

Open
henryiii wants to merge 4 commits into
pybind:masterfrom
henryiii:henryiii/feat/clihelper
Open

feat: small command line helpers for quick tests#4272
henryiii wants to merge 4 commits into
pybind:masterfrom
henryiii:henryiii/feat/clihelper

Conversation

@henryiii

@henryiii henryiii commented Oct 21, 2022

Copy link
Copy Markdown
Collaborator

Description

These are some small command line helpers intended for quick testing, based on python-config.
On a Unix system, it should be possible to do:

c++ pybindclass.cpp $(pybind11-config --cflags) $(pybind11-config --ldflags) -o pybindclass.$(python-config --extension-suffix)

Or even:

c++ $(pybind11-config --file=pybindclass.cpp)

This is handy for quick tests on issue reports.

Since I have to debug embedded code too, I also added support for --embed:

c++ $(pybind11-config --embed --file=pybindclass.cpp)

Based on https://github.com/python/cpython/blob/3.11/Misc/python-config.in

Suggested changelog entry:

* Small extra features added to the pybind11 command line tool based on python-config.

I can probably come up with a test or two.


📚 Documentation preview 📚: https://pybind11--4272.org.readthedocs.build/

@henryiii
henryiii marked this pull request as draft October 22, 2022 04:06
@henryiii
henryiii force-pushed the henryiii/feat/clihelper branch from 2a0be18 to 108e975 Compare October 25, 2022 16:45
@henryiii
henryiii force-pushed the henryiii/feat/clihelper branch from 108e975 to 13ec9d9 Compare July 22, 2026 23:47
@henryiii

Copy link
Copy Markdown
Collaborator Author

🤖 AI text below 🤖

Rebased on current master as a single commit. Changes beyond the rebase:

  • Fixed a LIBIDRLIBDIR typo in the embed link flags.
  • Fixed the Py_ENABLE_SHARED condition: python-config adds -L$LIBPL for static builds, not shared ones. Verified by compiling and running an embedded program against a static (Homebrew framework) Python.
  • -std=c++11-std=c++17 (v3 minimum), and it is now always added, not only when CFLAGS is set.
  • --extension is now --file with --embed support, matching the final design in the PR description; the file path keeps its directory and output paths are quoted like the existing flags.
  • Kept master's --extension-suffix (added separately in the meantime) and shared its include-dir logic via a new get_include_dirs().
  • Added tests in tests/extra_python_package/test_files.py and a short section in docs/compiling.rst.

@henryiii
henryiii marked this pull request as ready for review July 23, 2026 01:15
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
@henryiii
henryiii force-pushed the henryiii/feat/clihelper branch from 13ec9d9 to f4afa90 Compare July 25, 2026 03:03
@Skylion007

Copy link
Copy Markdown
Collaborator

@claude review this PR

@henryiii

Copy link
Copy Markdown
Collaborator Author

Ran manually with Fable medium:

🤖 AI text below 🤖

Review: PR #4272 — feat: small command line helpers for quick tests

Overview

Adds python-config-style flags to python -m pybind11: --cflags, --ldflags, --embed, and --file=<src> (which prints a full compiler command-line suffix including -o <output>). The flag-building logic lands in pybind11/commands.py (with print_includes refactored to use the new get_include_dirs()), plus docs and four packaging tests. Nice, focused quality-of-life feature that matches the PR description.

Correctness

  • Quoting is inconsistent, and the main flags are unquoted. print_includes() quotes each -I entry, but get_cflags() builds f"-I{d}" with no quoting, and get_ldflags() never quotes LIBDIR/LIBPL. A Python prefix or venv path containing a space (common on macOS/Windows user dirs) produces broken output for exactly the new flags, while --includes still handles it. Suggest applying quote() (or moving it into commands.py) inside get_cflags/get_ldflags. Note the split output for --file also mixes quoted (filename, -o target) and unquoted (flags) parts.
  • --embed output name on Windowssuffix = "" yields -o example with no .exe. The whole feature is clearly Unix-oriented (gcc/clang flag style, -lpython…), which is fine, but the help text or docs should say so; --ldflags on MSVC is meaningless yet prints something plausible-looking.
  • Flag combination semantics are slightly surprising: --includes --cflags silently drops --includes (the elif), and --includes --file also skips includes. Harmless in practice since --cflags supersedes it, but worth a comment or making them compose predictably.
  • get_ldflags(embed=True) correctly mirrors python-config (LIBDIR, LIBPL for static builds, -lpython{VERSION}{abiflags}, LIBS, SYSLIBS) — this part looks right, including free-threaded t abiflag via sys.abiflags.
  • Hardcoded -std=c++17 is appropriate for v3 (minimum standard), and appending it after CFLAGS means it wins if CFLAGS carries a -std= — fine.

Code quality / conventions

  • The refactor of print_includes into get_include_dirs() is a clean deduplication.
  • get_cflags/get_ldflags return pre-joined strings while every other commands.py getter returns paths/lists. Returning list[str] and joining in __main__.py would make the API more composable (e.g. for setup_helpers reuse later) — minor.
  • _get_config_var(name, fmt) returning a one-element list of a possibly multi-flag string (e.g. LIBS = "-lpthread -ldl") is fine for space-joining but is a foot-gun if anyone later treats these as argv elements. A short comment would help.
  • The interleaved print(..., end=" " if args.file else "\n") chain in main() works but is hard to follow; building the parts in a list and printing once would be clearer.

Tests

  • Good coverage of the four modes, and platform-guarding of the -shared assertion is right.
  • test_cli_ldflags_embed asserting only -lpython is weak — it would pass even if LIBDIR/LIBPL handling broke. Consider asserting -L presence when LIBDIR is set.
  • No test for the quoting path (a dir with spaces) — which is exactly where the current implementation breaks; a unit test calling get_cflags with a monkeypatched sysconfig would catch it.
  • Placement in tests/extra_python_package/test_files.py means they only run under nox -s tests_packaging, which is the right home for CLI tests.

Docs

  • The compiling.rst addition is clear. One nit: it says --file prints "everything after the compiler", but the user must still remember the output lands next to the source file (it uses the source path's directory), and that this is Unix-only. A one-line caveat would prevent issue reports.

Verdict

Solid, useful feature with correct python-config semantics for the embed case. The one thing I'd fix before merge is quoting in get_cflags/get_ldflags — it's an inconsistency with the existing --includes behavior and a real breakage for paths with spaces. Everything else is polish.

henryiii added 2 commits July 25, 2026 00:21
- 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
- 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
@henryiii

Copy link
Copy Markdown
Collaborator Author

Also ran /simplify:

🤖 AI text below 🤖

Done — pushed 5eef4faf. The four review agents (reuse, simplification, efficiency, altitude) had heavily overlapping findings; here's what was fixed and what I deliberately skipped.

Fixed:

  • _get_config_var(name, fmt, quote) three-knob API (flagged by 3 agents) — replaced with a plain _config(name) -> str helper that returns a stripped config var or ""; each call site now formats and quotes explicitly (flags.append(_quote(f"-L{libdir}"))), so the quoted-vs-passthrough rule is visible where it applies instead of hidden in a boolean.
  • Import cost on import pybind11 (efficiency) — commands.py is on the package's import path, and the new top-level sysconfig/shlex imports added ~1ms for every consumer that just wants get_include(). Both are now imported lazily inside the functions that need them; pybind11.commands import time went from ~1050µs back to ~78µs.
  • EXT_SUFFIX fetched twice in main() — hoisted to one ext_suffix variable used by both --file and --extension-suffix (which as a bonus no longer prints the literal string "None" if the var were unset).
  • importlib file-loading in the quoting test (flagged by all 4 agents) — replaced with monkeypatch.syspath_prepend + a normal import pybind11.commands, dropping the loader boilerplate and the separate module identity.
  • Stale comment — the CFLAGS passthrough comment no longer mentions the ld-side vars, which have their own comment in get_ldflags.

Skipped, with reasons:

  • Reuse STD_TMPL/MACOS from setup_helpers.py, or LDSHARED from sysconfigsetup_helpers.py is intentionally standalone-copyable, so importing it from commands.py inverts that design; and LDSHARED/LDCXXSHARED contain the full compiler command, not just flags, so they aren't a drop-in source for --ldflags.
  • Drop the --includes suppression / restructure with mutually-exclusive groups, and move the --file assembly into a commands.get_build_command() — both change the PR's intended CLI behavior/API surface beyond a cleanup; the suppression is one commented line.
  • Merge the two from .commands import blocks and alias shlex.quote directly — ruff's isort config itself enforces the split blocks, and the _quote wrapper is what lets the shlex import stay lazy (and keeps mypy's no-implicit-reexport happy).
  • Deduplicate the one-line -I join between print_includes and get_cflags — a shared helper would add more API than the one duplicated line it removes.

Verified after the changes: prek -a --quiet clean, all 6 CLI tests pass, and a real module compiled and ran via c++ $(python3 -m pybind11 --file=ex.cpp).

@henryiii

Copy link
Copy Markdown
Collaborator Author

And GPT-5.6-sol:

🤖 AI text below 🤖

The new extension command omits required shared-library flags on Unix platforms other than Linux and macOS, despite claiming general Unix compiler support.

Review comment:

  • [P2] Add shared-library flags for other Unix platforms — /Users/henryfs/git/pybind/pybind11/pybind11/commands.py:136-139
    On supported Unix platforms other than Linux and macOS (for example FreeBSD), this branch adds no -shared or -fPIC, so --file attempts to link the extension as an executable and fails due to the missing main. The CLI and documentation describe support for Unix-style GCC/Clang generally, so the extension flags should not be restricted to these two sys.platform values.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants