diff --git a/.github/workflows/python-test.yml b/.github/workflows/test.yml similarity index 73% rename from .github/workflows/python-test.yml rename to .github/workflows/test.yml index eb3ca20..645a9c0 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,3 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - name: Test on: @@ -71,7 +68,7 @@ jobs: uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 #v4 with: name: clang-tools-pip_wheel - path: dist/*.whl + path: dist/clang_tools*.whl install: needs: [build] @@ -101,7 +98,7 @@ jobs: - name: Install clang-tools binaries run: clang-tools --install ${{ matrix.version }} --tool clang-format clang-tidy clang-query clang-apply-replacements - - name: Show path of binaries + - name: Show path of clang-tools binaries shell: bash run: | if [ "${{ matrix.version }}" = "15" -o "${{ matrix.version }}" = "16" ] && [ "${{ matrix.os }}" = "windows-latest" ]; then @@ -116,7 +113,7 @@ jobs: which "clang-apply-replacements-${{ matrix.version }}" fi - - name: Check clang-tools on Windows + - name: Check clang-tools binaries on Windows if: matrix.os == 'windows-latest' shell: bash run: | @@ -135,7 +132,7 @@ jobs: ;; esac - - name: Check clang-tools on Unix + - name: Check clang-tools binaries on Unix if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' run: | if [ "${{ matrix.version }}" = "12.0.1" -a "${{ matrix.os }}" = "ubuntu-latest" ]; then @@ -150,6 +147,50 @@ jobs: clang-apply-replacements-${{ matrix.version }} --version fi + - name: Install and check clang-format wheels + if: ${{ matrix.version != '12.0.1' && fromJSON(matrix.version) >= 10 }} # Skip 12.0.1 + shell: bash + run: | + set -e + clang-tools-wheel --tool clang-format --version ${{ matrix.version }} + + echo "Checking clang-format versions..." + clang-format --version + + # Verify versions contain expected string + clang_format_version=$(clang-format --version) + + echo "clang-format version: $clang_format_version" + + if ! echo "$clang_format_version" | grep -q "version ${{ matrix.version }}"; then + echo "❌ Unexpected clang-format version!" + exit 1 + fi + + echo "✅ clang-format Versions are correct." + + - name: Install and check clang-tidy wheels + if: ${{ matrix.version != '12.0.1' && fromJSON(matrix.version) >= 13 }} + shell: bash + run: | + set -e + clang-tools-wheel --tool clang-tidy --version ${{ matrix.version }} + + echo "Checking clang-tidy versions..." + clang-tidy --version + + # Verify versions contain expected string + clang_tidy_version=$(clang-tidy --version) + + echo "clang-tidy version: $clang_tidy_version" + + if ! echo "$clang_tidy_version" | grep -q "version ${{ matrix.version }}"; then + echo "❌ Unexpected clang-tidy version!" + exit 1 + fi + + echo "✅ clang-tidy Versions are correct." + docs: uses: cpp-linter/.github/.github/workflows/sphinx.yml@main with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc59073..522b669 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace exclude: \.output @@ -12,13 +12,13 @@ repos: - id: debug-statements - id: requirements-txt-fixer - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.21.0 hooks: - id: pyupgrade - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.11 + rev: v0.14.3 hooks: - - id: ruff + - id: ruff-check - id: ruff-format # - repo: local # hooks: diff --git a/README.rst b/README.rst index 1117701..2846020 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,12 @@ clang-tools CLI =============== -**Install clang-format, clang-tidy, clang-query, and clang-apply-replacements binaries with clang-tools CLI.** - .. |latest-version| image:: https://img.shields.io/pypi/v/clang-tools?color=blue :target: https://pypi.org/project/clang-tools/ :alt: PyPI -.. |python-test| image:: https://github.com/cpp-linter/clang-tools-pip/actions/workflows/python-test.yml/badge.svg - :target: https://github.com/cpp-linter/clang-tools-pip/actions/workflows/python-test.yml - :alt: Python test +.. |test| image:: https://github.com/cpp-linter/clang-tools-pip/actions/workflows/test.yml/badge.svg + :target: https://github.com/cpp-linter/clang-tools-pip/actions/workflows/test.yml + :alt: test .. |codecov-badge| image:: https://codecov.io/gh/cpp-linter/clang-tools-pip/branch/main/graph/badge.svg?token=40G5ZOIRRR :target: https://codecov.io/gh/cpp-linter/clang-tools-pip :alt: codecov @@ -22,16 +20,23 @@ clang-tools CLI :target: https://pypistats.org/packages/clang-tools :alt: PyPI - Downloads -|latest-version| |python-test| |codecov-badge| |sonar-badge| |platform-badge| |pypi-badge| +|latest-version| |test| |codecov-badge| |sonar-badge| |platform-badge| |pypi-badge| + + +Easily install clang-format, clang-tidy, clang-query, and clang-apply-replacements static binaries or Python wheels using the ``clang-tools`` CLI. + .. important:: This package only manages binary executables (& corresponding symbolic links) that are installed using this package's executable script. It does not intend to change or modify any binary executable installed from other sources (like LLVM releases). + For Python wheels, this CLI only support clang-format and clang-tidy tools. + Features -------- +- Support clang tools binaries and Python wheels. - Binaries are statically linked for improved portability. - Binaries can be specified installed for increased flexibility. - Binaries are checked with SHA512 checksum. This ensures: @@ -48,6 +53,7 @@ Features category. - Customizable install path. + Install clang-tools CLI ----------------------- @@ -71,26 +77,27 @@ Install clang-tools CLI 2. the installed path (for MacOS and Windows) is within the environment's variable ``PATH``. -Install `clang-tools` command with pip +Install ``clang-tools`` command with pip .. code-block:: shell pip install clang-tools -Install `clang-tools` from git repo +Install ``clang-tools`` from git repo .. code-block:: shell pip install git+https://github.com/cpp-linter/clang-tools-pip.git@main + CLI Usage --------- For a list of supported Command Line Interface options, see `the CLI documentation `_ -Examples -******** +Install binaries examples +~~~~~~~~~~~~~~~~~~~~~~~~~ Use ``clang-tools`` command to install version 13 binaries. @@ -126,13 +133,42 @@ If the installed directory is in your path, you can run the installed tools. Default target: x86_64-unknown-linux-gnu Host CPU: skylake -Supported versions + +Install wheels examples +~~~~~~~~~~~~~~~~~~~~~~~~~ + +After installing the ``clang-tools`` CLI, you can install the Python wheels using the ``clang-tools-wheel`` command. + +.. important:: + + The ``clang-tools-wheel`` command is primarily intended for cpp-linter projects to simplify installing clang tools Python wheels. + For general use, it is recommended to install the wheels directly using ``pip``, ``pipx``, ``uv``, or similar tools. + + +.. code-block:: shell + + # Install latest clang-format wheel + clang-tools-wheel --tool clang-format + # Install specific version clang-format wheel + clang-tools-wheel --tool clang-format --version 21 + + # Install latest clang-tidy wheel + clang-tools-wheel --tool clang-tidy + # Install specific version clang-tidy wheel + clang-tools-wheel --tool clang-tidy --version 21 + + +Supported Versions ------------------ -clang-format, clang-tidy, clang-query, clang-apply-replacements -*************************************************************** + +clang tools binaries +~~~~~~~~~~~~~~~~~~~~ + +The following table shows the supported versions of clang-format, clang-tidy, clang-query, and clang-apply-replacements binaries for each platform: + .. csv-table:: - :header: "Version", "21", "20", "19", "18", "17", "16", "15", "14", "13", "12", "11", "10", "9" + :header: "Platform", "21", "20", "19", "18", "17", "16", "15", "14", "13", "12", "11", "10", "9" :stub-columns: 1 Linux,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️ @@ -140,3 +176,13 @@ clang-format, clang-tidy, clang-query, clang-apply-replacements macOS,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️,✔️ For more details, visit the `clang-tools-static-binaries `_ repository. + +clang tools Python wheels +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following Python wheels are supported: + +- `clang-format `_ +- `clang-tidy `_ + +Check the respective PyPI pages for available versions and platform support. diff --git a/clang_tools/wheel.py b/clang_tools/wheel.py new file mode 100644 index 0000000..c8f9b7d --- /dev/null +++ b/clang_tools/wheel.py @@ -0,0 +1,36 @@ +from argparse import ArgumentParser +from cpp_linter_hooks.util import resolve_install + + +def get_parser() -> ArgumentParser: + """Get a parser to interpret CLI args.""" + parser = ArgumentParser(description="Install specified clang tool wheel") + parser.add_argument( + "--tool", + required=True, + choices=["clang-format", "clang-tidy"], + help="Tool to install (clang-format or clang-tidy)", + ) + parser.add_argument( + "--version", + default=None, + help="Version to install (e.g., 21 or 21.1.2). Defaults to latest compatible version.", + ) + return parser + + +def main() -> int: + parser = get_parser() + args = parser.parse_args() + path = resolve_install(args.tool, args.version) + version_str = f" version {args.version}" if args.version else " latest version" + if path: + print(f"{args.tool}{version_str} installed at: {path}") + return 0 + else: + print(f"Failed to install {args.tool}{version_str}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/api.rst b/docs/api.rst index e10d56f..e177401 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,3 +9,6 @@ API Reference .. automodule:: clang_tools.util :members: + +.. automodule:: clang_tools.wheel + :members: diff --git a/docs/conf.py b/docs/conf.py index f91fe21..415377e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,6 +14,7 @@ from sphinx.util.docutils import SphinxRole from sphinx_immaterial.inline_icons import load_svg_into_builder_env from clang_tools.main import get_parser +from clang_tools.wheel import get_parser as get_wheel_parser # -- Path setup -------------------------------------------------------------- @@ -207,38 +208,49 @@ def setup(app: Sphinx): app.add_role("badge-default", CliBadgeDefault()) app.add_role("badge-switch", CliBadgeSwitch()) + def write_cli_doc(doc_path, parser, prog_name): + with open(doc_path, mode="w") as doc: + doc.write(f"{prog_name} --help\n{'=' * 30}\n\n") + doc.write( + ".. code-block:: text\n :caption: Usage\n :class: no-copy\n\n" + ) + parser.prog = prog_name + str_buf = StringIO() + parser.print_usage(str_buf) + usage = str_buf.getvalue() + start = usage.find(parser.prog) + for line in usage.splitlines(): + doc.write(f" {line[start:]}\n") + args = parser._optionals._actions + for arg in args: + aliases = arg.option_strings + if not aliases or arg.default == "==SUPPRESS==": + continue + assert arg.help is not None + doc.write("\n.. std:option:: " + ", ".join(aliases) + "\n") + req_ver = next( + ( + ver + for ver, names in REQUIRED_VERSIONS.items() + if arg.dest in names + ), + "0.1.0", + ) + doc.write(f"\n :badge-version:`{req_ver}` ") + if arg.default: + default = ( + " ".join(arg.default) + if isinstance(arg.default, list) + else arg.default + ) + doc.write(f":badge-default:`{default}` ") + if isinstance(arg, _StoreTrueAction): + doc.write(":badge-switch:`Accepts no value` ") + doc.write("\n\n ") + doc.write("\n ".join(arg.help.splitlines()) + "\n") + cli_doc = Path(app.srcdir, "cli_args.rst") - with open(cli_doc, mode="w") as doc: - doc.write("Command Line Interface Options\n==============================\n\n") - parser = get_parser() - doc.write(".. code-block:: text\n :caption: Usage\n :class: no-copy\n\n") - parser.prog = "clang-tools" - str_buf = StringIO() - parser.print_usage(str_buf) - usage = str_buf.getvalue() - start = usage.find(parser.prog) - for line in usage.splitlines(): - doc.write(f" {line[start:]}\n") - - args = parser._optionals._actions - for arg in args: - aliases = arg.option_strings - if not aliases or arg.default == "==SUPPRESS==": - continue - assert arg.help is not None - doc.write("\n.. std:option:: " + ", ".join(aliases) + "\n") - req_ver = "0.1.0" - for ver, names in REQUIRED_VERSIONS.items(): - if arg.dest in names: - req_ver = ver - break - doc.write(f"\n :badge-version:`{req_ver}` ") - if arg.default: - default = arg.default - if isinstance(arg.default, list): - default = " ".join(arg.default) - doc.write(f":badge-default:`{default}` ") - if isinstance(arg, _StoreTrueAction): - doc.write(":badge-switch:`Accepts no value` ") - doc.write("\n\n ") - doc.write("\n ".join(arg.help.splitlines()) + "\n") + write_cli_doc(cli_doc, get_parser(), "clang-tools") + + wheel_cli_doc = Path(app.srcdir, "wheel_cli_args.rst") + write_cli_doc(wheel_cli_doc, get_wheel_parser(), "clang-tools-wheel") diff --git a/docs/index.rst b/docs/index.rst index 03b61da..56f28a1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ :hidden: cli_args + wheel_cli_args .. toctree:: :hidden: diff --git a/docs/usage.rst b/docs/usage.rst index 7fb9428..e8bf318 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,5 +1,5 @@ -Using a Custom Binary Repository --------------------------------- +Custom Binary Repository +------------------------ You can override the default source for downloading **clang-tools** by setting the following environment variables: diff --git a/docs/wheel_cli_args.rst b/docs/wheel_cli_args.rst new file mode 100644 index 0000000..ec98580 --- /dev/null +++ b/docs/wheel_cli_args.rst @@ -0,0 +1,20 @@ +clang-tools-wheel --help +============================== + +.. code-block:: text + :caption: Usage + :class: no-copy + + clang-tools-wheel [-h] [--tool {clang-format,clang-tidy}] [--version VERSION] + +.. std:option:: --tool + + :badge-version:`0.11.0` :badge-default:`clang-format` + + Tool to install (clang-format or clang-tidy) + +.. std:option:: --version + + :badge-version:`0.1.0` + + Version to install (e.g., 21 or 21.1.2). Defaults to latest compatible version. diff --git a/pyproject.toml b/pyproject.toml index ab5308b..d331739 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,16 @@ classifiers = [ "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Build Tools", ] + +dependencies = [ + "cpp-linter-hooks>=1.1.7", +] + dynamic = ["version"] [project.scripts] clang-tools = "clang_tools.main:main" +clang-tools-wheel = "clang_tools.wheel:main" [project.urls] source = "https://github.com/cpp-linter/clang-tools-pip" diff --git a/tests/test_wheel.py b/tests/test_wheel.py new file mode 100644 index 0000000..56cf27b --- /dev/null +++ b/tests/test_wheel.py @@ -0,0 +1,37 @@ +import sys +from clang_tools.wheel import main + + +def test_main_success(monkeypatch): + # Patch resolve_install to simulate success + monkeypatch.setattr( + "clang_tools.wheel.resolve_install", + lambda tool, version: "/usr/bin/clang-format", + ) + monkeypatch.setattr( + sys, "argv", ["wheel.py", "--tool", "clang-format", "--version", "15.0.7"] + ) + exit_code = main() + assert exit_code == 0 + + +def test_main_failure(monkeypatch): + # Patch resolve_install to simulate failure + monkeypatch.setattr("clang_tools.wheel.resolve_install", lambda tool, version: None) + monkeypatch.setattr( + sys, "argv", ["wheel.py", "--tool", "clang-format", "--version", "99.99.99"] + ) + exit_code = main() + assert exit_code == 1 + + +def test_main_default_tool(monkeypatch): + # Patch resolve_install to simulate success for default tool + monkeypatch.setattr( + "clang_tools.wheel.resolve_install", + lambda tool, version: "/usr/bin/clang-format", + ) + # The CLI requires --tool; simulate running with the default tool explicitly + monkeypatch.setattr(sys, "argv", ["wheel.py", "--tool", "clang-format"]) + exit_code = main() + assert exit_code == 0