Skip to content

Commit d31f76c

Browse files
committed
feat: unify CLI entry point - eliminate binary/wheel split
BREAKING CHANGE: The separate command is removed. Wheel installation is now available through the unified command. New subcommand-based CLI: clang-tools install <version> # auto-detect binary/wheel clang-tools install <version> --binary # force binary clang-tools install <version> --wheel # force wheel clang-tools install <tool-name> --wheel # wheel install by tool name clang-tools uninstall <version> # remove installed tools Auto-detection logic: - Version-like targets in supported range → binary first, fallback wheel - Version-like targets out of range → wheel - Tool name targets → wheel Legacy / flags are preserved for backward compatibility.
1 parent f9e7f18 commit d31f76c

8 files changed

Lines changed: 774 additions & 100 deletions

File tree

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,17 @@ For a full list of CLI options, see the [documentation](https://cpp-linter.githu
7171
### Install binaries
7272

7373
```bash
74-
# Install version 13 binaries
75-
clang-tools --install 13
74+
# Install version 13 binaries (auto-detects binary vs wheel)
75+
clang-tools install 13
76+
77+
# Force binary installation
78+
clang-tools install 13 --binary
7679

7780
# Install to a specified directory
78-
clang-tools --install 13 --directory .
81+
clang-tools install 13 --directory .
7982

8083
# Install specific tools
81-
clang-tools --install 14 --tool clang-format clang-query
84+
clang-tools install 14 --tool clang-format clang-query
8285
```
8386

8487
If the installed directory is in your path:
@@ -92,14 +95,14 @@ clang-format-13 --version
9295

9396
```bash
9497
# Install latest clang-format wheel
95-
clang-tools-wheel --tool clang-format
98+
clang-tools install clang-format --wheel
9699

97100
# Install specific version
98-
clang-tools-wheel --tool clang-format --version 21
101+
clang-tools install clang-format --wheel --version 21
99102
```
100103

101104
> [!IMPORTANT]
102-
> The `clang-tools-wheel` command is primarily intended for
105+
> Wheel installation is primarily intended for
103106
> cpp-linter projects. For general use, install wheels directly
104107
> using `pip`, `pipx`, or `uv`.
105108

clang_tools/main.py

Lines changed: 269 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,77 @@
22
``clang_tools.main``
33
--------------------
44
5-
The module containing main entrypoint function.
5+
The module containing the unified main entrypoint function.
6+
Supports both binary (static build) and wheel-based installation
7+
via a single ``clang-tools`` command.
68
"""
79

810
import argparse
11+
import sys
912

1013
from .install import install_clang_tools, uninstall_clang_tools
11-
from . import RESET_COLOR, YELLOW
14+
from . import RESET_COLOR, YELLOW, MIN_VERSION, MAX_VERSION
1215
from .util import Version
1316

1417

18+
def _is_version_like(target: str) -> bool:
19+
"""Check if *target* looks like a version number (e.g. ``"18"``, ``"18.1"``).
20+
"""
21+
try:
22+
parts = target.split(".")
23+
for p in parts:
24+
int(p)
25+
return True
26+
except ValueError:
27+
return False
28+
29+
30+
#: Known tool names supported by wheel installs
31+
WHEEL_TOOLS = {"clang-format", "clang-tidy", "clang-query", "clang-apply-replacements"}
32+
33+
34+
def _wheel_install(tools: list[str], version: str | None) -> int:
35+
"""Install tool(s) via wheel (cpp_linter_hooks).
36+
37+
:returns: exit code (0 on success, 1 on failure).
38+
"""
39+
from cpp_linter_hooks.util import resolve_install # lazy import
40+
41+
ok = True
42+
for tool in tools:
43+
path = resolve_install(tool, version)
44+
version_str = f" version {version}" if version else " latest version"
45+
if path:
46+
print(f"{tool}{version_str} installed at: {path}")
47+
else:
48+
print(f"Failed to install {tool}{version_str}", file=sys.stderr)
49+
ok = False
50+
return 0 if ok else 1
51+
52+
1553
def get_parser() -> argparse.ArgumentParser:
16-
"""Get and parser to interpret CLI args."""
17-
parser = argparse.ArgumentParser()
54+
"""Get a parser to interpret CLI args (unified entry point)."""
55+
parser = argparse.ArgumentParser(
56+
description="Install and manage clang-tools (clang-format, clang-tidy, etc.)"
57+
)
1858

59+
# ------------------------------------------------------------------
60+
# Backward-compatible top-level flags (deprecated – kept for
61+
# compatibility with older scripts / workflows)
62+
# ------------------------------------------------------------------
1963
parser.add_argument(
2064
"-i",
2165
"--install",
2266
metavar="VERSION",
23-
help="Install clang-tools about a specific version. This can be in the form of"
24-
" a semantic version specification (``x.y.z``, ``x.y``, ``x``). NOTE: A "
25-
"malformed version specification will cause a silent failure.",
67+
help="(deprecated) Use 'clang-tools install <version>' instead",
68+
dest="_legacy_install",
69+
)
70+
parser.add_argument(
71+
"-u",
72+
"--uninstall",
73+
metavar="VERSION",
74+
help="(deprecated) Use 'clang-tools uninstall <version>' instead",
75+
dest="_legacy_uninstall",
2676
)
2777
parser.add_argument(
2878
"-t",
@@ -43,54 +93,236 @@ def get_parser() -> argparse.ArgumentParser:
4393
"-f",
4494
"--overwrite",
4595
action="store_true",
46-
help="Force overwriting the symlink to the installed binary. This will only "
47-
"overwrite an existing symlink.",
96+
help="Force overwriting the symlink to the installed binary.",
4897
)
4998
parser.add_argument(
5099
"-b",
51100
"--no-progress-bar",
52101
action="store_true",
53102
help="Do not display a progress bar for downloads.",
54103
)
55-
parser.add_argument(
56-
"-u",
57-
"--uninstall",
58-
metavar="VERSION",
59-
help="Uninstall clang-tools with specific version. "
60-
"This is done before any install.",
104+
105+
# ------------------------------------------------------------------
106+
# Subcommands (new-style CLI)
107+
# ------------------------------------------------------------------
108+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
109+
110+
# --- ``clang-tools install <target>`` ---------------------------------
111+
install_p = subparsers.add_parser("install", help="Install clang-tools")
112+
install_p.add_argument(
113+
"target",
114+
help="Version number (e.g. 18) or tool name (e.g. clang-format)",
115+
)
116+
install_p.add_argument(
117+
"--binary",
118+
action="store_true",
119+
help="Force binary (static build) installation",
120+
)
121+
install_p.add_argument(
122+
"--wheel",
123+
action="store_true",
124+
help="Force wheel installation via cpp-linter-hooks",
125+
)
126+
install_p.add_argument(
127+
"--version",
128+
dest="explicit_version",
129+
default=None,
130+
metavar="VER",
131+
help="Explicit version for wheel install (when target is a tool name)",
132+
)
133+
install_p.add_argument(
134+
"-t",
135+
"--tool",
136+
nargs="+",
137+
default=["clang-format", "clang-tidy"],
138+
metavar="TOOL",
139+
help="Specify which tool(s) to install.",
140+
)
141+
install_p.add_argument(
142+
"-d",
143+
"--directory",
144+
default="",
145+
metavar="DIR",
146+
help="The directory where the clang-tools are installed.",
61147
)
148+
install_p.add_argument(
149+
"-f",
150+
"--overwrite",
151+
action="store_true",
152+
help="Force overwriting the symlink to the installed binary.",
153+
)
154+
install_p.add_argument(
155+
"-b",
156+
"--no-progress-bar",
157+
action="store_true",
158+
help="Do not display a progress bar for downloads.",
159+
)
160+
161+
# --- ``clang-tools uninstall <version>`` ------------------------------
162+
uninstall_p = subparsers.add_parser("uninstall", help="Uninstall clang-tools")
163+
uninstall_p.add_argument("version", help="Version to uninstall")
164+
uninstall_p.add_argument(
165+
"-t",
166+
"--tool",
167+
nargs="+",
168+
default=["clang-format", "clang-tidy"],
169+
metavar="TOOL",
170+
help="Specify which tool(s) to uninstall.",
171+
)
172+
uninstall_p.add_argument(
173+
"-d",
174+
"--directory",
175+
default="",
176+
metavar="DIR",
177+
help="The directory from which to uninstall the tools.",
178+
)
179+
62180
return parser
63181

64182

65-
def main():
66-
"""The main entrypoint to the CLI program."""
183+
def main() -> int:
184+
"""Unified entry point for the CLI program.
185+
186+
:returns: exit code (0 on success, 1 on failure).
187+
"""
67188
parser = get_parser()
68189
args = parser.parse_args()
69190

70-
if args.uninstall:
71-
uninstall_clang_tools(args.uninstall, args.tool, args.directory)
72-
elif args.install:
73-
version = Version(args.install)
74-
if version.info != (0, 0, 0):
75-
install_clang_tools(
76-
version,
77-
args.tool,
78-
args.directory,
79-
args.overwrite,
80-
args.no_progress_bar,
81-
)
82-
else:
83-
print(
84-
f"{YELLOW}The version specified is not a semantic",
85-
f"specification{RESET_COLOR}",
191+
# ---- Legacy backward-compat path ----------------------------------
192+
if args._legacy_uninstall or args._legacy_install:
193+
if args._legacy_uninstall:
194+
uninstall_clang_tools(
195+
args._legacy_uninstall, args.tool, args.directory
86196
)
87-
else:
197+
if args._legacy_install:
198+
version = Version(args._legacy_install)
199+
if version.info != (0, 0, 0):
200+
install_clang_tools(
201+
version,
202+
args.tool,
203+
args.directory,
204+
args.overwrite,
205+
args.no_progress_bar,
206+
)
207+
else:
208+
print(
209+
f"{YELLOW}The version specified is not a semantic",
210+
f"specification{RESET_COLOR}",
211+
file=sys.stderr,
212+
)
213+
return 1
214+
return 0
215+
216+
# ---- No subcommand, no legacy flags → nothing to do --------------
217+
if args.command is None:
88218
print(
89-
f"{YELLOW}Nothing to do because `--install` and `--uninstall`",
90-
f"was not specified.{RESET_COLOR}",
219+
f"{YELLOW}Nothing to do. Use 'install' or 'uninstall' subcommand,"
220+
f" or --install/--uninstall flags.{RESET_COLOR}",
221+
file=sys.stderr,
91222
)
92223
parser.print_help()
224+
return 0
225+
226+
# ---- ``uninstall`` subcommand ------------------------------------
227+
if args.command == "uninstall":
228+
uninstall_clang_tools(args.version, args.tool, args.directory)
229+
return 0
230+
231+
# ---- ``install`` subcommand --------------------------------------
232+
if args.command == "install":
233+
target: str = args.target
234+
binary: bool = args.binary
235+
wheel: bool = args.wheel
236+
237+
# safety: --binary and --wheel are mutually exclusive
238+
if binary and wheel:
239+
print(
240+
f"{YELLOW}Error: --binary and --wheel are mutually"
241+
f" exclusive{RESET_COLOR}",
242+
file=sys.stderr,
243+
)
244+
return 1
245+
246+
# ---- Case: --wheel (target may be tool name or version) -----
247+
if wheel:
248+
if _is_version_like(target):
249+
# ``clang-tools install 18 --wheel``
250+
return _wheel_install(args.tool, target)
251+
else:
252+
# ``clang-tools install clang-format --wheel``
253+
# Install the target as the tool (ignore default --tool)
254+
return _wheel_install([target], args.explicit_version)
255+
256+
# ---- Case: --binary (target must be a version) --------------
257+
if binary:
258+
if not _is_version_like(target):
259+
print(
260+
f"{YELLOW}Error: --binary requires a version number"
261+
f" (got '{target}'){RESET_COLOR}",
262+
file=sys.stderr,
263+
)
264+
return 1
265+
version = Version(target)
266+
if version.info != (0, 0, 0):
267+
install_clang_tools(
268+
version,
269+
args.tool,
270+
args.directory,
271+
args.overwrite,
272+
args.no_progress_bar,
273+
)
274+
return 0
275+
print(
276+
f"{YELLOW}The version specified is not a semantic"
277+
f" specification{RESET_COLOR}",
278+
file=sys.stderr,
279+
)
280+
return 1
281+
282+
# ---- Auto-detect (no --binary or --wheel flag) --------------
283+
if _is_version_like(target):
284+
version = Version(target)
285+
if version.info != (0, 0, 0):
286+
# try binary first, fall back to wheel
287+
try:
288+
install_clang_tools(
289+
version,
290+
args.tool,
291+
args.directory,
292+
args.overwrite,
293+
args.no_progress_bar,
294+
)
295+
return 0
296+
except Exception as exc:
297+
print(
298+
f"{YELLOW}Binary install failed"
299+
f" ({exc}), falling back to"
300+
f" wheel...{RESET_COLOR}",
301+
file=sys.stderr,
302+
)
303+
# fallback to wheel
304+
return _wheel_install(args.tool, target)
305+
else:
306+
print(
307+
f"{YELLOW}The version specified is not a semantic"
308+
f" specification{RESET_COLOR}",
309+
file=sys.stderr,
310+
)
311+
return 1
312+
else:
313+
# target is a tool name → install via wheel
314+
if target not in WHEEL_TOOLS:
315+
print(
316+
f"{YELLOW}Unknown target '{target}'. Expected a"
317+
f" version number or one of: "
318+
f"{', '.join(sorted(WHEEL_TOOLS))}{RESET_COLOR}",
319+
file=sys.stderr,
320+
)
321+
return 1
322+
return _wheel_install([target], args.explicit_version)
323+
324+
return 0 # unreachable
93325

94326

95327
if __name__ == "__main__":
96-
main()
328+
raise SystemExit(main())

0 commit comments

Comments
 (0)