Skip to content

Commit f345167

Browse files
committed
Migrate from clang-tools binaries to wheels
1 parent 0134570 commit f345167

File tree

9 files changed

+223
-68
lines changed

9 files changed

+223
-68
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ result.txt
1313
testing/main.c
1414
*/*compile_commands.json
1515

16-
# Ignore clang-tools binaries
16+
# Ignore Python wheel packages (clang-format, clang-tidy)
1717
clang-tidy-1*
1818
clang-tidy-2*
1919
clang-format-1*

MIGRATION.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Migration Guide: From clang-tools to Python Wheels
2+
3+
## Overview
4+
5+
Starting from version 0.9.0, `cpp-linter-hooks` has migrated from using the `clang-tools` package to using Python wheel packages for `clang-format` and `clang-tidy`. This change provides better cross-platform compatibility, easier installation, and more reliable dependency management.
6+
7+
## What Changed
8+
9+
### Dependencies
10+
- **Before**: Used `clang-tools==0.15.1` package
11+
- **After**: Uses `clang-format` and `clang-tidy` wheel packages from PyPI
12+
13+
### Installation Method
14+
- **Before**: clang-format and clang-tidy were installed via `clang-tools` package which managed binaries
15+
- **After**: clang-format and clang-tidy are installed as Python packages and available as executables
16+
17+
### Benefits of Migration
18+
19+
1. **Better Cross-Platform Support**: Python wheels work consistently across different operating systems
20+
2. **Simplified Installation**: No need to manage binary installations separately
21+
3. **More Reliable**: No more issues with binary compatibility or single threaded execution
22+
4. **Better Version Management**: Each tool version is a separate package release
23+
24+
## Breaking Changes
25+
26+
### For End Users
27+
28+
- **No breaking changes**: The pre-commit hook configuration remains exactly the same
29+
- All existing `.pre-commit-config.yaml` files will continue to work without modification
30+
31+
### For Developers
32+
- The internal `ensure_installed()` function now returns the tool name instead of a Path object
33+
- The `util.py` module has been rewritten to use `shutil.which()` instead of `clang_tools.install`
34+
- Tests have been updated to mock the new wheel-based installation
35+
36+
## Migration Steps
37+
38+
### For End Users
39+
No action required! Your existing configuration will continue to work.
40+
41+
### For Developers/Contributors
42+
1. Update your development environment:
43+
```bash
44+
pip install clang-format clang-tidy
45+
```
46+
47+
2. If you were importing from the utility module:
48+
```python
49+
# Before
50+
from cpp_linter_hooks.util import ensure_installed
51+
path = ensure_installed("clang-format", "18")
52+
command = [str(path), "--version"]
53+
54+
# After
55+
from cpp_linter_hooks.util import ensure_installed
56+
tool_name = ensure_installed("clang-format", "18")
57+
command = [tool_name, "--version"]
58+
```
59+
60+
## Version Support
61+
62+
The wheel packages support the same LLVM versions as before:
63+
- LLVM 16, 17, 18, 19, 20+
64+
- The `--version` argument continues to work as expected
65+
66+
## Troubleshooting
67+
68+
### Tool Not Found Error
69+
If you encounter "command not found" errors:
70+
71+
1. Ensure the wheel packages are installed:
72+
```bash
73+
pip install clang-format clang-tidy
74+
```
75+
76+
2. Verify the tools are available:
77+
```bash
78+
clang-format --version
79+
clang-tidy --version
80+
```
81+
82+
3. Check that the tools are in your PATH:
83+
```bash
84+
which clang-format
85+
which clang-tidy
86+
```
87+
88+
### Version Mismatch
89+
If you need a specific version, you can install it explicitly:
90+
```bash
91+
pip install clang-format==18.1.8
92+
pip install clang-tidy==18.1.8
93+
```
94+
95+
## Support
96+
97+
If you encounter any issues after the migration, please:
98+
1. Check this migration guide
99+
2. Search existing [issues](https://github.com/cpp-linter/cpp-linter-hooks/issues)
100+
3. Create a new issue with:
101+
- Your operating system
102+
- Python version
103+
- The exact error message
104+
- Your `.pre-commit-config.yaml` configuration
105+
106+
## References
107+
108+
- [clang-format wheel package](https://github.com/ssciwr/clang-format-wheel)
109+
- [clang-tidy wheel package](https://github.com/ssciwr/clang-tidy-wheel)
110+
- [PyPI clang-format](https://pypi.org/project/clang-format/)
111+
- [PyPI clang-tidy](https://pypi.org/project/clang-tidy/)

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ repos:
5454

5555
### Custom Clang Tool Version
5656

57-
To use specific versions of [clang-tools](https://github.com/cpp-linter/clang-tools-pip?tab=readme-ov-file#supported-versions):
57+
To use specific versions of clang-format and clang-tidy (using Python wheel packages):
5858

5959
```yaml
6060
repos:
@@ -67,6 +67,9 @@ repos:
6767
args: [--checks=.clang-tidy, --version=18] # Specifies version
6868
```
6969

70+
> [!NOTE]
71+
> Starting from version 0.9.0, this package uses Python wheel packages ([clang-format](https://pypi.org/project/clang-format/) and [clang-tidy](https://pypi.org/project/clang-tidy/)) instead of the previous clang-tools binaries. The wheel packages provide better cross-platform compatibility and easier installation.
72+
7073
## Output
7174

7275
### clang-format Output

cpp_linter_hooks/clang_format.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
def run_clang_format(args=None) -> Tuple[int, str]:
1717
hook_args, other_args = parser.parse_known_args(args)
18-
path = ensure_installed("clang-format", hook_args.version)
19-
command = [str(path), "-i"]
18+
tool_name = ensure_installed("clang-format", hook_args.version)
19+
command = [tool_name, "-i"]
2020

2121
# Add verbose flag if requested
2222
if hook_args.verbose:

cpp_linter_hooks/clang_tidy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
def run_clang_tidy(args=None) -> Tuple[int, str]:
1313
hook_args, other_args = parser.parse_known_args(args)
14-
path = ensure_installed("clang-tidy", hook_args.version)
15-
command = [str(path)]
14+
tool_name = ensure_installed("clang-tidy", hook_args.version)
15+
command = [tool_name]
1616
command.extend(other_args)
1717

1818
retval = 0

cpp_linter_hooks/util.py

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,64 @@
11
import sys
2+
import shutil
23
from pathlib import Path
34
import logging
45
from typing import Optional
56

6-
from clang_tools.util import Version
7-
from clang_tools.install import is_installed as _is_installed, install_tool
8-
9-
107
LOG = logging.getLogger(__name__)
118

9+
DEFAULT_CLANG_VERSION = "20" # Default version for clang tools, can be overridden
1210

13-
DEFAULT_CLANG_VERSION = "18" # Default version for clang tools, can be overridden
1411

15-
16-
def is_installed(tool_name: str, version: str) -> Optional[Path]:
12+
def is_installed(tool_name: str, version: str = "") -> Optional[Path]:
1713
"""Check if tool is installed.
1814
19-
Checks the current python prefix and PATH via clang_tools.install.is_installed.
15+
With wheel packages, the tools are installed as regular Python packages
16+
and available via shutil.which().
2017
"""
21-
# check in current python prefix (usual situation when we installed into pre-commit venv)
22-
directory = Path(sys.executable).parent
23-
path = directory / f"{tool_name}-{version}"
24-
if path.is_file():
25-
return path
26-
27-
# parse the user-input version as a string
28-
parsed_ver = Version(version)
29-
# also check using clang_tools
30-
path = _is_installed(tool_name, parsed_ver)
31-
if path is not None:
32-
return Path(path)
18+
# Check if tool is available in PATH
19+
tool_path = shutil.which(tool_name)
20+
if tool_path is not None:
21+
return Path(tool_path)
22+
23+
# Check if tool is available in current Python environment
24+
if sys.executable:
25+
python_dir = Path(sys.executable).parent
26+
tool_path = python_dir / tool_name
27+
if tool_path.is_file():
28+
return tool_path
29+
30+
# Also check Scripts directory on Windows
31+
scripts_dir = python_dir / "Scripts"
32+
if scripts_dir.exists():
33+
tool_path = scripts_dir / tool_name
34+
if tool_path.is_file():
35+
return tool_path
36+
# Try with .exe extension on Windows
37+
tool_path = scripts_dir / f"{tool_name}.exe"
38+
if tool_path.is_file():
39+
return tool_path
3340

34-
# not found
3541
return None
3642

3743

38-
def ensure_installed(tool_name: str, version: str = DEFAULT_CLANG_VERSION) -> Path:
44+
def ensure_installed(tool_name: str, version: str = "") -> str:
3945
"""
40-
Ensure tool is available at given version.
46+
Ensure tool is available. With wheel packages, we assume the tools are
47+
installed as dependencies and available in PATH.
48+
49+
Returns the tool name (not path) since the wheel packages install the tools
50+
as executables that can be called directly.
4151
"""
42-
LOG.info("Checking for %s, version %s", tool_name, version)
52+
LOG.info("Checking for %s", tool_name)
4353
path = is_installed(tool_name, version)
4454
if path is not None:
45-
LOG.info("%s, version %s is already installed", tool_name, version)
46-
return path
55+
LOG.info("%s is available at %s", tool_name, path)
56+
return tool_name # Return tool name for direct execution
4757

48-
LOG.info("Installing %s, version %s", tool_name, version)
49-
directory = Path(sys.executable).parent
50-
install_tool(tool_name, version, directory=str(directory), no_progress_bar=True)
51-
return directory / f"{tool_name}-{version}"
58+
# If not found, we'll still return the tool name and let subprocess handle the error
59+
LOG.warning(
60+
"%s not found in PATH. Make sure the %s wheel package is installed.",
61+
tool_name,
62+
tool_name,
63+
)
64+
return tool_name

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ requires-python = ">=3.9"
66

77
[project]
88
name = "cpp_linter_hooks"
9-
description = "Automatically formats and lints C/C++ code using clang-format and clang-tidy"
9+
description = "Automatically formats and lints C/C++ code using clang-format and clang-tidy Python wheels"
1010
readme = "README.md"
1111
keywords = ["clang", "clang-format", "clang-tidy", "pre-commit", "pre-commit-hooks"]
1212
license = "MIT"
@@ -32,7 +32,8 @@ classifiers = [
3232
"Topic :: Software Development :: Build Tools",
3333
]
3434
dependencies = [
35-
"clang-tools==0.15.1",
35+
"clang-format",
36+
"clang-tidy",
3637
]
3738
dynamic = ["version"]
3839

testing/main.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
#include <stdio.h>
2-
int main() {for (;;) break; printf("Hello world!\n");return 0;}
2+
int main() {
3+
for (;;) break;
4+
printf("Hello world!\n");
5+
return 0;
6+
}

tests/test_util.py

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import sys
33
import pytest
44
from itertools import product
5+
from unittest.mock import patch
56

6-
from cpp_linter_hooks.util import ensure_installed, DEFAULT_CLANG_VERSION
7+
from cpp_linter_hooks.util import ensure_installed, is_installed
78

89

910
VERSIONS = [None, "18"]
@@ -13,38 +14,60 @@
1314
@pytest.mark.benchmark
1415
@pytest.mark.parametrize(("tool", "version"), list(product(TOOLS, VERSIONS)))
1516
def test_ensure_installed(tool, version, tmp_path, monkeypatch, caplog):
16-
bin_path = tmp_path / "bin"
17+
"""Test that ensure_installed returns the tool name for wheel packages."""
1718
with monkeypatch.context() as m:
18-
m.setattr(sys, "executable", str(bin_path / "python"))
19+
m.setattr(sys, "executable", str(tmp_path / "bin" / "python"))
1920

20-
for run in range(2):
21-
# clear any existing log messages
21+
# Mock shutil.which to simulate the tool being available
22+
with patch("shutil.which", return_value=str(tmp_path / tool)):
2223
caplog.clear()
2324
caplog.set_level(logging.INFO, logger="cpp_linter_hooks.util")
2425

2526
if version is None:
26-
ensure_installed(tool)
27+
result = ensure_installed(tool)
2728
else:
28-
ensure_installed(tool, version=version)
29-
30-
bin_version = version or DEFAULT_CLANG_VERSION
31-
assert (bin_path / f"{tool}-{bin_version}").is_file
32-
33-
# first run should install
34-
assert (
35-
caplog.record_tuples[0][2]
36-
== f"Checking for {tool}, version {bin_version}"
37-
)
38-
if run == 0:
39-
# FIXME
40-
# assert caplog.record_tuples[1][2] == f"Installing {tool}, version {bin_version}"
41-
assert (
42-
caplog.record_tuples[1][2]
43-
== f"{tool}, version {bin_version} is already installed"
44-
)
45-
# second run should just confirm it's already installed
46-
else:
47-
assert (
48-
caplog.record_tuples[1][2]
49-
== f"{tool}, version {bin_version} is already installed"
50-
)
29+
result = ensure_installed(tool, version=version)
30+
31+
# Should return the tool name for direct execution
32+
assert result == tool
33+
34+
# Check that we logged checking for the tool
35+
assert any("Checking for" in record.message for record in caplog.records)
36+
37+
38+
def test_is_installed_with_shutil_which(tmp_path):
39+
"""Test is_installed when tool is found via shutil.which."""
40+
tool_path = tmp_path / "clang-format"
41+
tool_path.touch()
42+
43+
with patch("shutil.which", return_value=str(tool_path)):
44+
result = is_installed("clang-format")
45+
assert result == tool_path
46+
47+
48+
def test_is_installed_not_found():
49+
"""Test is_installed when tool is not found anywhere."""
50+
with (
51+
patch("shutil.which", return_value=None),
52+
patch("sys.executable", "/nonexistent/python"),
53+
):
54+
result = is_installed("clang-format")
55+
assert result is None
56+
57+
58+
def test_ensure_installed_tool_not_found(caplog):
59+
"""Test ensure_installed when tool is not found."""
60+
with (
61+
patch("shutil.which", return_value=None),
62+
patch("sys.executable", "/nonexistent/python"),
63+
):
64+
caplog.clear()
65+
caplog.set_level(logging.WARNING, logger="cpp_linter_hooks.util")
66+
67+
result = ensure_installed("clang-format")
68+
69+
# Should still return the tool name
70+
assert result == "clang-format"
71+
72+
# Should log a warning
73+
assert any("not found in PATH" in record.message for record in caplog.records)

0 commit comments

Comments
 (0)