Skip to content

Commit df98241

Browse files
committed
Add $(python,...) built-in
This introduces a portable $(python,...) preprocessor function that evaluates a Python code string in-process via exec(), returning "y" on success and "n" on exception. 'exec' namespace exposes os, sys, shutil, platform, and a shell-free run(*argv) helper for subprocess checks. Each call receives a fresh globals copy for namespace isolation. Rewrite all six toolchain functions (cc-option, ld-option, as-instr, as-option, cc-option-bit, rustc-option) to use subprocess.Popen with argument lists (shell=False) and os.devnull, eliminating /dev/null, 2>/dev/null, and printf-pipe constructs. This closes the shell injection vector from environment variables and makes the functions work on Windows. Add _run_argv() helper with timeout support and proper process cleanup. Remove dead _run_cmd_in_tmpdir(); replaced by TemporaryDirectory context managers in individual functions. Add quote tracking to the macro expander so that commas and parentheses inside single, double, and triple-quoted strings are not treated as argument separators (required for $(python,assert "a,b" == "a,b")). Close #41
1 parent ee37b32 commit df98241

File tree

5 files changed

+551
-136
lines changed

5 files changed

+551
-136
lines changed

.github/workflows/test.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,17 @@ jobs:
8888
path: ${{ matrix.target.headless-only && '.' || 'Kconfiglib' }}
8989

9090
- name: Run pytest selftests
91-
# Skip on Windows: several tests depend on Unix shell ($(shell,...)),
92-
# gcc, and forward-slash paths that are unavailable on Windows CI.
93-
if: ${{ matrix.target.headless-only != true }}
9491
working-directory: ${{ matrix.target.headless-only && '.' || 'Kconfiglib' }}
9592
run: |
96-
python -m pytest tests/ -v --tb=short --ignore=tests/test_conformance.py
93+
if [ "${{ matrix.target.headless-only }}" == "true" ]; then
94+
# Windows: portable subset -- excludes tests that need gcc or
95+
# Unix shell commands (Kpreprocess uses $(shell,...) with
96+
# bash-specific syntax).
97+
python -m pytest tests/test_preprocess.py -v --tb=short \
98+
-k "test_user_defined or test_success_failure or test_python_fn or test_kconfig_warn"
99+
else
100+
python -m pytest tests/ -v --tb=short --ignore=tests/test_conformance.py
101+
fi
97102
98103
- name: Apply Linux Kconfig Makefile patch
99104
# Skip for Windows (headless-only mode)

KBUILD.md

Lines changed: 154 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Kbuild Toolchain Functions
22

3-
Kconfiglib implements Kbuild toolchain detection functions used by the Linux kernel since version 4.18.
3+
Kconfiglib implements Kbuild toolchain detection functions used by the Linux kernel since version 4.18,
4+
plus the portable `$(python,...)` built-in for cross-platform boolean checks.
45
These preprocessor functions enable runtime detection of compiler, assembler, and linker capabilities,
56
allowing kernel configurations to adapt to different toolchain versions.
67

@@ -26,6 +27,39 @@ For comprehensive Kconfig syntax documentation, see the
2627
`$(failure,command)`
2728
: Returns `n` if command succeeds, `y` otherwise. Inverse of `success`.
2829

30+
### Portable In-Process Checks
31+
32+
`$(python,code)`
33+
: Evaluates a Python code string in-process via `exec()`. Returns `y` if
34+
execution succeeds without exception, `n` otherwise. No subprocess, no shell,
35+
no PATH dependency. Use `assert` for boolean checks.
36+
37+
The exec namespace provides pre-imported modules and a shell-free subprocess helper:
38+
39+
| Name | Type | Purpose |
40+
|------------|----------|--------------------------------------------|
41+
| `os` | module | env vars, paths, file ops |
42+
| `sys` | module | platform, version, `sys.executable` path |
43+
| `shutil` | module | `which()` for tool detection |
44+
| `platform` | module | machine/system/architecture |
45+
| `run` | function | shell-free subprocess, returns bool |
46+
47+
`run(*argv)` executes a command as an argument list (`shell=False`) and returns
48+
`True` if it exits with code 0, `False` otherwise.
49+
50+
Each `exec()` call receives a fresh copy of the namespace so assignments in one
51+
`$(python,...)` invocation do not leak into subsequent calls.
52+
53+
`SystemExit` is handled specially: `SystemExit(0)` and `SystemExit(None)` map
54+
to `y`; non-zero/non-empty codes map to `n`. `AssertionError` maps to `n`
55+
silently (expected for boolean checks via `assert`). All other exceptions
56+
(`NameError`, `SyntaxError`, etc.) map to `n` and emit a Kconfig warning
57+
with the exception type and message, aiding diagnosis of typos in code strings.
58+
59+
Trust model: `$(python,...)` has the same trust level as `$(shell,...)`.
60+
Kconfig files are trusted code (like Makefiles). The restricted globals provide
61+
scope isolation (no parser internals visible), not security sandboxing.
62+
2963
### Compiler Detection
3064

3165
`$(cc-option,flag[,fallback])`
@@ -99,6 +133,48 @@ config HAS_32BIT
99133
def_bool "$(m32-flag)" != ""
100134
```
101135

136+
### Portable Checks with $(python,...)
137+
138+
```
139+
# Boolean checks (in-process, no subprocess)
140+
config PYTHON_AVAILABLE
141+
def_bool $(python,)
142+
143+
config HAS_CC
144+
def_bool $(python,assert os.environ.get('CC'))
145+
146+
config IS_LINUX
147+
def_bool $(python,assert sys.platform == 'linux')
148+
149+
config IS_X86_64
150+
def_bool $(python,assert platform.machine() == 'x86_64')
151+
152+
config HAS_GCC
153+
def_bool $(python,assert shutil.which('gcc'))
154+
155+
# Shell-free subprocess checks
156+
config CC_IS_CLANG
157+
def_bool $(python,assert run(sys.executable, 'scripts/detect-compiler.py', '--is', 'Clang'))
158+
159+
config HAVE_SDL2
160+
def_bool $(python,assert run('pkg-config', '--exists', 'sdl2'))
161+
```
162+
163+
Commas inside `run(...)` are safe: the Kconfig preprocessor tracks parenthesis
164+
depth and only splits on commas at the top level of the function call.
165+
166+
Quoted strings are also safe: the preprocessor tracks single (`'`), double (`"`),
167+
triple-single (`'''`), and triple-double (`"""`) quoted regions. Commas and
168+
parentheses inside quotes are treated as literal characters, not argument
169+
separators or nesting markers. Backslash escapes (`\"`, `\'`) inside quoted
170+
regions are handled correctly.
171+
172+
Use semicolons instead of commas for multi-statement code:
173+
`$(python,import os; assert os.path.isfile('Makefile'))`.
174+
175+
For string-valued results (e.g., getting the compiler type name), `$(shell,...)`
176+
remains the right tool. `$(python,...)` only returns `y` or `n`.
177+
102178
## Implementation
103179

104180
### Design
@@ -110,6 +186,28 @@ Functions are implemented in `kconfiglib.py` following these principles:
110186
- Python 3.6+ using standard library only
111187
- Graceful error handling (missing tools return `n`)
112188

189+
### Shell-Free Toolchain Functions
190+
191+
Toolchain functions (`cc-option`, `ld-option`, `as-instr`, `as-option`,
192+
`cc-option-bit`, `rustc-option`) use `subprocess.Popen` with argument lists
193+
(`shell=False`) and `os.devnull` instead of Unix shell syntax. This
194+
eliminates shell injection from environment variables and Kconfig-supplied
195+
options, and makes the functions portable to Windows.
196+
197+
Internal helpers:
198+
199+
`_run_argv(argv, stdin_data=None)`
200+
: Runs a command as an argument list. Returns `True` if exit code is 0.
201+
Used by all toolchain functions.
202+
203+
`_run_cmd(command)`
204+
: Runs a command via shell (`shell=True`). Used by `success`, `failure`,
205+
and `if-success`, which accept user-supplied shell commands by design.
206+
207+
`_run_helper(*argv)`
208+
: Shell-free subprocess for `$(python,...)` code strings. Exposed as `run()`
209+
in the exec namespace.
210+
113211
### Environment Variables
114212

115213
Functions respect standard build variables:
@@ -119,33 +217,34 @@ Functions respect standard build variables:
119217

120218
### Performance
121219

122-
Functions execute shell commands during Kconfig parsing, which can be slow.
123-
For applications that parse configurations repeatedly, consider implementing
124-
caching or using `allow_empty_macros=True` to skip toolchain detection.
220+
Toolchain functions spawn subprocesses during Kconfig parsing, which can be
221+
slow. `$(python,...)` checks that don't call `run()` execute in-process with
222+
no subprocess overhead. For applications that parse configurations repeatedly,
223+
consider implementing caching or using `allow_empty_macros=True` to skip
224+
toolchain detection.
125225

126226
## Testing
127227

128-
Four test suites validate the implementation:
228+
Tests live in `tests/test_preprocess.py` (part of the pytest suite):
129229

130-
`test_issue111.py`
131-
: Validates basic toolchain function parsing.
230+
`test_kbuild_functions`
231+
: Verifies toolchain functions (`cc-option`, `as-instr`, etc.) and
232+
`$(python,...)` via Kconfig parsing. Exercises the full preprocessor path.
132233

133-
`test_issue109.py`
134-
: Tests nested function calls and complex expressions.
234+
`test_success_failure_fns`
235+
: Tests `success`, `failure`, and `if-success` directly in Python using
236+
`sys.executable` as a portable true/false replacement.
135237

136-
`test_kbuild_complete.py`
137-
: Comprehensive suite with 35+ test cases covering all functions, edge cases, and error conditions.
238+
`test_python_fn_isolation`
239+
: Verifies that variable assignments in one `$(python,...)` call do not
240+
leak into subsequent calls.
138241

139-
`test_kernel_compat.py`
140-
: Real-world kernel Kconfig snippets from init/Kconfig, arch/x86/Kconfig, etc.
242+
`test_python_fn_system_exit`
243+
: Verifies `SystemExit` handling: `exit(0)` maps to `y`, non-zero to `n`.
141244

142-
Run all tests:
245+
Run:
143246
```bash
144-
python3 test_basic_parsing.py && \
145-
python3 test_issue111.py && \
146-
python3 test_issue109.py && \
147-
python3 test_kbuild_complete.py && \
148-
python3 test_kernel_compat.py
247+
python3 -m pytest tests/test_preprocess.py -v
149248
```
150249

151250
## Compatibility
@@ -165,6 +264,30 @@ Tested with:
165264
- binutils 2.31+
166265
- rustc 1.60+ (optional)
167266

267+
## Portability
268+
269+
### Unix vs Windows
270+
271+
| Unix shell idiom | Portable replacement |
272+
|---|---|
273+
| `$(shell,cmd 2>/dev/null && echo y \|\| echo n)` | `$(python,assert run('cmd', 'arg'))` |
274+
| `$(shell,test -n "$CC" && echo y \|\| echo n)` | `$(python,assert os.environ.get('CC'))` |
275+
| `$(shell,scripts/foo.py --flag ...)` | `$(python,assert run(sys.executable, 'scripts/foo.py', '--flag'))` |
276+
| `$(shell,pkg-config --exists lib && echo y \|\| echo n)` | `$(python,assert run('pkg-config', '--exists', 'lib'))` |
277+
| `$(success,true)` | `$(python,)` |
278+
| `$(failure,false)` | `$(python,assert False)` |
279+
280+
`$(shell,...)` remains necessary for string-valued output (e.g., compiler
281+
type name, version strings). For boolean checks, prefer `$(python,...)`
282+
on cross-platform projects.
283+
284+
### Toolchain functions
285+
286+
`cc-option`, `ld-option`, `as-instr`, `as-option`, `cc-option-bit`, and
287+
`rustc-option` are portable by default. They use `subprocess.Popen` with
288+
argument lists internally -- no shell involvement, no `/dev/null` path
289+
dependency (`os.devnull` is used instead).
290+
168291
## Real-World Examples
169292

170293
From `arch/x86/Kconfig.cpu`:
@@ -191,6 +314,18 @@ config SHADOW_CALL_STACK
191314
depends on $(cc-option,-fsanitize=shadow-call-stack -ffixed-x18)
192315
```
193316

317+
From a cross-platform project (Mado):
318+
```
319+
config CC_IS_CLANG
320+
def_bool $(python,assert run(sys.executable, 'scripts/detect-compiler.py', '--is', 'Clang'))
321+
322+
config HAVE_SDL2
323+
def_bool $(python,assert run('pkg-config', '--exists', 'sdl2'))
324+
325+
config CROSS_COMPILE_ENABLED
326+
def_bool $(python,assert os.environ.get('CROSS_COMPILE'))
327+
```
328+
194329
## See Also
195330

196331
- [Kconfig Language](https://docs.kernel.org/kbuild/kconfig-language.html) - Complete syntax specification

0 commit comments

Comments
 (0)