diff --git a/AGENTS.md b/AGENTS.md index caf0180..eea1a52 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ uv run # Auto-rebuilds the extension if ne # Testing uv run run_tests.py # Main golden-master suite: C++ tests + paired Python comparison uv run run_tests.py --regressions # Python regression scripts in tests/regressions/ -uv run run_llvm_c_tests.py # Vendored llvm-c-test lit tests (C binary) +uv run run_llvm_c_tests.py # Vendored llvm-c-test lit tests (auto-rebuilds C binary) uv run run_llvm_c_tests.py --use-python # Vendored llvm-c-test lit tests with Python implementation uv run run_llvm_c_tests.py -v # Run lit tests with verbose output ./build/test_factorial # Run single C++ test @@ -20,8 +20,8 @@ uv run test_factorial.py # Run single Python test uv run pytest tests/regressions/... # Target a specific regression file or subset uv run tests/test_module.py # Python tests are intended to be directly runnable scripts uv run tests/regressions/test_const_bytes.py # Regression scripts should also run directly via __main__ -uv run llvm-c-test --targets-list # Run llvm-c-test directly (dev-only tool) -./llvm-c-test --echo < input.bc # Can also invoke directly (auto-finds venv) +uv run python -m llvm_c_test --targets-list # Run Python llvm-c-test port directly (dev-only tool) +./build/llvm-c-test --echo < input.bc # Run vendored C llvm-c-test binary directly uvx ty check # Type check Python code (not a test runner) uvx ty check llvm_c_test/ # Type check specific directory diff --git a/README.md b/README.md index cece584..140fccd 100644 --- a/README.md +++ b/README.md @@ -82,12 +82,16 @@ uv run run_tests.py uv run run_tests.py --regressions # Vendored llvm-c-test lit suite against the C test binary +# Rebuilds the vendored C binary before running lit. uv run run_llvm_c_tests.py uv run run_llvm_c_tests.py -v # Vendored llvm-c-test lit suite against the Python implementation uv run run_llvm_c_tests.py --use-python +# Run the Python llvm-c-test port directly during development +uv run python -m llvm_c_test --targets-list + # Type checking (not a test suite, but commonly run in CI/dev) uvx ty check ``` diff --git a/devdocs/archive/llvm-c-test.md b/devdocs/archive/llvm-c-test.md index 9903807..5b1b592 100644 --- a/devdocs/archive/llvm-c-test.md +++ b/devdocs/archive/llvm-c-test.md @@ -22,7 +22,7 @@ Port the LLVM-C test suite (`llvm-c-test`) from C/C++ to Python as a **drop-in r ``` llvm_c_test/ ├── __init__.py -├── __main__.py # CLI entry point (uv run llvm-c-test) +├── __main__.py # CLI entry point (uv run python -m llvm_c_test) ├── main.py # Command dispatcher ├── helpers.py # tokenize_stdin(), shared utilities ├── targets.py # --targets-list diff --git a/llvm-c/llvm-c-test/inputs/lit.cfg.py b/llvm-c/llvm-c-test/inputs/lit.cfg.py index 0cdc3df..c60176b 100644 --- a/llvm-c/llvm-c-test/inputs/lit.cfg.py +++ b/llvm-c/llvm-c-test/inputs/lit.cfg.py @@ -34,6 +34,11 @@ if not llvm_tools_dir: if not llvm_c_test_cmd: lit_config.fatal("LLVM_C_TEST_CMD environment variable not set") +# Forward PYTHONPATH for the dev-only Python llvm-c-test implementation. +# The package is intentionally not installed into site-packages. +if pythonpath := os.environ.get("PYTHONPATH"): + config.environment["PYTHONPATH"] = pythonpath + # Tool substitutions # On Windows, wrap individual tool paths with quotes to handle backslashes in bash diff --git a/llvm_c_test/__main__.py b/llvm_c_test/__main__.py index b5f5cc2..f4d64ae 100644 --- a/llvm_c_test/__main__.py +++ b/llvm_c_test/__main__.py @@ -2,9 +2,9 @@ CLI entry point for llvm-c-test Python port. Usage: - uv run llvm-c-test --targets-list - uv run llvm-c-test --calc < calc.test - uv run llvm-c-test --module-dump < input.bc + uv run python -m llvm_c_test --targets-list + uv run python -m llvm_c_test --calc < calc.test + uv run python -m llvm_c_test --module-dump < input.bc """ import sys diff --git a/pyproject.toml b/pyproject.toml index f4f1338..ab4d15f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,9 +10,6 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [] -[project.scripts] -llvm-c-test = "llvm_c_test.main:main" - [project.urls] Homepage = "https://github.com/LLVMParty/llvm-nanobind" @@ -33,14 +30,17 @@ minimum-version = "build-system.requires" # Setuptools-style build caching in a local directory build-dir = "build/{wheel_tag}" +# Package builds only need the extension and generated stubs. +build.targets = ["llvm_stub"] + # Build stable ABI wheels for CPython 3.12+ wheel.py-api = "cp312" # This setup only works properly in redirect mode editable.mode = "redirect" -# Install the llvm_c_test Python package -wheel.packages = ["llvm_c_test"] +# Do not auto-copy Python packages into wheels; the extension module is installed by CMake. +wheel.packages = [] [[tool.scikit-build.overrides]] if.platform-system = "win32" @@ -51,9 +51,6 @@ cache-keys = [ { file = "CMakeLists.txt" }, { file = "src/**/*.cpp" }, { file = "src/**/*.h" }, - { file = "llvm-c/llvm-c-test/*.c" }, - { file = "llvm-c/llvm-c-test/*.cpp" }, - { file = "llvm-c/llvm-c-test/*.h" }, ] [tool.ty.src] @@ -103,3 +100,4 @@ exclude_lines = [ [tool.pytest.ini_options] addopts = "--deselect tests/regressions/test_symbol_size_crash.py" +pythonpath = ["."] diff --git a/run_llvm_c_tests.py b/run_llvm_c_tests.py index 99c52fc..2f79fcd 100755 --- a/run_llvm_c_tests.py +++ b/run_llvm_c_tests.py @@ -15,6 +15,9 @@ Options: --llvm-prefix PATH Path to LLVM installation prefix --use-python Use Python implementation instead of C binary +When running against the C binary, this script rebuilds the vendored +llvm-c-test target before invoking lit. + Examples: python run_llvm_c_tests.py # Run all tests (C binary) python run_llvm_c_tests.py --use-python # Run with Python implementation @@ -114,14 +117,60 @@ def parse_args(args: list[str]) -> tuple[str | None, bool, list[str]]: return llvm_prefix, use_python, remaining +def find_llvm_c_test_exe(build_dir: Path) -> Path: + """Find the vendored llvm-c-test executable in a CMake build directory.""" + exe_name = "llvm-c-test.exe" if sys.platform == "win32" else "llvm-c-test" + candidates = [ + build_dir / exe_name, + build_dir / "Release" / exe_name, + build_dir / "RelWithDebInfo" / exe_name, + build_dir / "Debug" / exe_name, + build_dir / "MinSizeRel" / exe_name, + ] + for candidate in candidates: + if candidate.exists(): + return candidate + return candidates[0] + + +def build_vendored_llvm_c_test(project_root: Path) -> Path: + """Rebuild the vendored C llvm-c-test target and return its executable path.""" + build_dir = project_root / "build" + cmd = ["cmake", "--build", str(build_dir), "--target", "llvm-c-test-vendored"] + + print("Building vendored llvm-c-test C binary...", flush=True) + result = subprocess.run(cmd, cwd=project_root) + if result.returncode != 0: + print( + f"Error: failed to build llvm-c-test-vendored (exit {result.returncode})", + file=sys.stderr, + ) + sys.exit(result.returncode) + + llvm_c_test_exe = find_llvm_c_test_exe(build_dir) + if not llvm_c_test_exe.exists(): + print( + f"Error: llvm-c-test executable not found after build: {llvm_c_test_exe}", + file=sys.stderr, + ) + print( + "Build with: cmake --build build --target llvm-c-test-vendored", + file=sys.stderr, + ) + sys.exit(1) + + return llvm_c_test_exe + + def build_llvm_c_test_cmd( - use_python: bool, project_root: Path + use_python: bool, project_root: Path, llvm_c_test_exe: Path | None = None ) -> tuple[str, dict[str, str]]: """Build the llvm-c-test command string and extra environment variables. Args: use_python: If True, use Python implementation; otherwise use C binary. project_root: Path to project root for locating files. + llvm_c_test_exe: Path to the vendored C binary when not using Python. Returns: Tuple of (command string, extra environment variables dict). @@ -129,6 +178,17 @@ def build_llvm_c_test_cmd( extra_env: dict[str, str] = {} if use_python: + # The llvm_c_test package is a dev-only source package, not an installed + # distribution entry point. Make it importable for lit subprocesses while + # preferring the freshly built extension module from build/. + pythonpath_entries = [ + str((project_root / "build").resolve()), + str(project_root.resolve()), + ] + if existing_pythonpath := os.environ.get("PYTHONPATH"): + pythonpath_entries.append(existing_pythonpath) + extra_env["PYTHONPATH"] = os.pathsep.join(pythonpath_entries) + # Check if we should enable coverage or logging coverage_run = os.environ.get("COVERAGE_RUN") @@ -145,22 +205,20 @@ def build_llvm_c_test_cmd( coverage_file_arg = coverage_file.as_posix() cmd = f"{python_exe} -m coverage run --parallel-mode --data-file={coverage_file_arg} -m llvm_c_test" else: - # Direct invocation using the installed script + # Direct invocation using the Python module cmd = f"{python_exe} -m llvm_c_test" return cmd, extra_env - else: - # Use C binary - llvm_c_test_exe = project_root / "build" / "llvm-c-test" - if sys.platform == "win32": - llvm_c_test_exe = llvm_c_test_exe.with_suffix(".exe") - # On Windows, quote the path to handle backslashes in bash - exe_path = str(llvm_c_test_exe) - if sys.platform == "win32": - exe_path = f'"{exe_path}"' + if llvm_c_test_exe is None: + llvm_c_test_exe = find_llvm_c_test_exe(project_root / "build") - return exe_path, extra_env + # On Windows, quote the path to handle backslashes in bash + exe_path = str(llvm_c_test_exe) + if sys.platform == "win32": + exe_path = f'"{exe_path}"' + + return exe_path, extra_env def prefer_windows_git_bash(env: dict[str, str]) -> str | None: @@ -200,28 +258,14 @@ def main(): print("Run: cmake -B build -G Ninja && cmake --build build", file=sys.stderr) sys.exit(1) - # Build the llvm-c-test command - llvm_c_test_cmd, extra_env = build_llvm_c_test_cmd(use_python, project_root) - - # Verify C binary exists if not using Python + llvm_c_test_exe = None if not use_python: - llvm_c_test_exe = project_root / "build" / "llvm-c-test" - if sys.platform == "win32": - llvm_c_test_exe = llvm_c_test_exe.with_suffix(".exe") + llvm_c_test_exe = build_vendored_llvm_c_test(project_root) - if not llvm_c_test_exe.exists(): - print( - f"Error: llvm-c-test executable not found: {llvm_c_test_exe}", - file=sys.stderr, - ) - print( - "Build with: cmake --build build --target llvm-c-test-vendored", - file=sys.stderr, - ) - print( - "Or use --use-python to run with Python implementation", file=sys.stderr - ) - sys.exit(1) + # Build the llvm-c-test command + llvm_c_test_cmd, extra_env = build_llvm_c_test_cmd( + use_python, project_root, llvm_c_test_exe + ) # Get LLVM tools directory llvm_prefix = get_llvm_prefix(llvm_prefix_arg) @@ -288,7 +332,7 @@ def main(): print(" Mode: Python implementation") else: print(" Mode: C binary") - print() + print(flush=True) result = subprocess.run(lit_args, env=env) sys.exit(result.returncode) diff --git a/run_tests.py b/run_tests.py index af47c35..4c67499 100755 --- a/run_tests.py +++ b/run_tests.py @@ -106,10 +106,16 @@ def run_python_test(script: Path) -> tuple[str, str, int]: # Build command with optional coverage wrapper cmd = coverage_wrap(script.stem, [str(script)]) + # Prefer the freshly built extension while keeping the repository root + # importable for dev-only helper packages such as tools.obfuscation. + pythonpath_entries = [str(BUILD_DIR.resolve()), str(Path(__file__).parent.resolve())] + if existing_pythonpath := os.environ.get("PYTHONPATH"): + pythonpath_entries.append(existing_pythonpath) + result = subprocess.run( [sys.executable] + cmd, capture_output=True, - env={**os.environ, "PYTHONPATH": str(BUILD_DIR)}, + env={**os.environ, "PYTHONPATH": os.pathsep.join(pythonpath_entries)}, ) try: stdout = result.stdout.decode("utf-8") diff --git a/tests/regressions/test_dibuilder_metadata.py b/tests/regressions/test_dibuilder_metadata.py index 623e0fc..4e2d3dd 100644 --- a/tests/regressions/test_dibuilder_metadata.py +++ b/tests/regressions/test_dibuilder_metadata.py @@ -31,6 +31,10 @@ is in llvm-c/llvm-c-test/debuginfo.c. import subprocess import sys +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[2] def get_function_dbg_id(output: str) -> str | None: @@ -50,19 +54,25 @@ def get_function_dbg_id(output: str) -> str | None: def run_python_dibuilder(): """Run Python --test-dibuilder and capture output.""" result = subprocess.run( - ["uv", "run", "llvm-c-test", "--test-dibuilder"], + [sys.executable, "-m", "llvm_c_test", "--test-dibuilder"], capture_output=True, text=True, + cwd=PROJECT_ROOT, ) return result.stdout def run_c_dibuilder(): """Run C --test-dibuilder and capture output.""" + exe = PROJECT_ROOT / "build" / "llvm-c-test" + if sys.platform == "win32": + exe = exe.with_suffix(".exe") + result = subprocess.run( - ["./build/llvm-c-test", "--test-dibuilder"], + [str(exe), "--test-dibuilder"], capture_output=True, text=True, + cwd=PROJECT_ROOT, ) return result.stdout diff --git a/tests/regressions/test_symbol_size_crash.py b/tests/regressions/test_symbol_size_crash.py index c90fceb..1966092 100644 --- a/tests/regressions/test_symbol_size_crash.py +++ b/tests/regressions/test_symbol_size_crash.py @@ -101,7 +101,7 @@ def test_symbol_size_crash(): print("\nTest completed (crash avoided by not accessing .size)") print("\nTo trigger the crash, uncomment the 'size = sym_iter.size' line") - print("or run: cat | llvm-c-test --object-list-symbols") + print("or run: cat | ./build/llvm-c-test --object-list-symbols") def test_with_existing_object(): diff --git a/uv.lock b/uv.lock index f86b65f..c6705a9 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" [[package]]