From 2b542ab971f7f485243879dcbccb53b7c5d9dea2 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Sat, 4 Jul 2026 12:16:17 +0300 Subject: [PATCH 1/2] fix(script): keep llvm-cov/profdata/symbolizer/scan-deps in generated formulae MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The update-formula generator only knew about clang-format, clang-tidy, clang-query, clang-apply-replacements and clang-include-cleaner. The llvm-cov, llvm-profdata, llvm-symbolizer and clang-scan-deps tools were added to the formulae by hand, so every automated regeneration dropped them again from the bundle formulae (see the auto-update PR that removed 230 lines). Teach the generator about all four extra tools so they are emitted as bundle resources (arm + intel), included in the install list, the `desc` and the test block, and generated as individual formulae — matching the current hand-maintained Formula/*.rb exactly. Regenerating now reproduces main instead of deleting tools. Also align the README with the other cpp-linter repos: add the shared "cpp-linter hub" badge plus Test Formulae / Update Formula CI badges in a centered header. --- README.md | 8 +++ scripts/update-formula.py | 113 +++++++++++++++++++++----------------- 2 files changed, 70 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 245a9c0..03b2125 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,15 @@ +
+ # cpp-linter Homebrew Tap +[![Test Formulae](https://github.com/cpp-linter/homebrew-tap/actions/workflows/test.yml/badge.svg)](https://github.com/cpp-linter/homebrew-tap/actions/workflows/test.yml) +[![Update Formula](https://github.com/cpp-linter/homebrew-tap/actions/workflows/update-formula.yml/badge.svg)](https://github.com/cpp-linter/homebrew-tap/actions/workflows/update-formula.yml) +[![cpp-linter hub](https://img.shields.io/badge/%F0%9F%8F%A0_cpp--linter_hub-%E2%86%90_home-22863a)](https://cpp-linter.github.io/) + Homebrew tap for [clang-tools-static-binaries](https://github.com/cpp-linter/clang-tools-static-binaries). +
+ Provides pre-built static binaries for: | Category | Tools | diff --git a/scripts/update-formula.py b/scripts/update-formula.py index cf14d55..f9d8be7 100644 --- a/scripts/update-formula.py +++ b/scripts/update-formula.py @@ -31,6 +31,10 @@ VERSIONS = [22, 21, 20, 19, 18] TOOLS_ALL = ["clang-format", "clang-tidy", "clang-query", "clang-apply-replacements"] TOOLS_IC = ["clang-include-cleaner"] # LLVM 18+ +# LLVM utility binaries + clang-scan-deps, shipped for every supported version. +# These live alongside the clang tools in each upstream release and must be kept +# in sync here, otherwise a regeneration would silently drop them. +TOOLS_EXTRA = ["llvm-cov", "llvm-profdata", "llvm-symbolizer", "clang-scan-deps"] PLATFORMS = ["macos-arm64", "macos-amd64"] # Individual tool formula metadata @@ -61,6 +65,26 @@ "bin": "clang-include-cleaner", "desc": "clang-include-cleaner", }, + "llvm-cov": { + "class_prefix": "LlvmCov", + "bin": "llvm-cov", + "desc": "llvm-cov", + }, + "llvm-profdata": { + "class_prefix": "LlvmProfdata", + "bin": "llvm-profdata", + "desc": "llvm-profdata", + }, + "llvm-symbolizer": { + "class_prefix": "LlvmSymbolizer", + "bin": "llvm-symbolizer", + "desc": "llvm-symbolizer", + }, + "clang-scan-deps": { + "class_prefix": "ClangScanDeps", + "bin": "clang-scan-deps", + "desc": "clang-scan-deps", + }, } SCRIPT_DIR = Path(__file__).resolve().parent @@ -102,6 +126,20 @@ def rel_url(tool: str, version: int, platform: str, release_tag: str) -> str: return asset_url(tool, version, platform, release_tag) +def bundle_resource_tools(version: int) -> list[str]: + """Tools bundled as Homebrew resources (everything except clang-format), in file order. + + clang-format is the primary download (url/sha on the formula body); every other + tool is emitted as a `resource` block. Order here defines the order in the + generated formula and must match the hand-maintained files. + """ + tools = ["clang-tidy", "clang-query", "clang-apply-replacements"] + if version >= 18: + tools.append("clang-include-cleaner") + tools += TOOLS_EXTRA + return tools + + # ── Bundle formula (clang-tools) ─────────────────────────────────────────────── @@ -114,11 +152,12 @@ def generate_bundle_formula( """Generate the content of the 'clang-tools' Homebrew formula (bundle).""" ver = version class_name = "ClangTools" if is_latest else f"ClangToolsAT{ver}" - include_cleaner = ver >= 18 - tools_desc = "clang-format, clang-tidy, clang-query, clang-apply-replacements" - if include_cleaner: - tools_desc += ", and clang-include-cleaner" + resource_tools = bundle_resource_tools(ver) + + # Human-readable description lists clang-format first, then every resource. + desc_tools = ["clang-format"] + resource_tools + tools_desc = ", ".join(desc_tools[:-1]) + ", and " + desc_tools[-1] lines: list[str] = [] @@ -128,59 +167,31 @@ def generate_bundle_formula( lines.append(f' version "{ver}"') lines.append("") - # ── ARM ── - lines.append(" on_arm do") - lines.append(f' url "{rel_url("clang-format", ver, "macos-arm64", release_tag)}"') - lines.append(f' sha256 "{sha(sha_map, "clang-format", ver, "macos-arm64")}"') - lines.append("") - - for tool in ["clang-tidy", "clang-query", "clang-apply-replacements"]: - lines.append(f' resource "{tool}" do') - lines.append(f' url "{rel_url(tool, ver, "macos-arm64", release_tag)}"') - lines.append(f' sha256 "{sha(sha_map, tool, ver, "macos-arm64")}"') - lines.append(" end") + # ── ARM / Intel share the same resource layout, differing only by platform ── + for on_block, platform in (("on_arm", "macos-arm64"), ("on_intel", "macos-amd64")): + lines.append(f" {on_block} do") + lines.append(f' url "{rel_url("clang-format", ver, platform, release_tag)}"') + lines.append(f' sha256 "{sha(sha_map, "clang-format", ver, platform)}"') lines.append("") - if include_cleaner: - lines.append(' resource "clang-include-cleaner" do') - lines.append(f' url "{rel_url("clang-include-cleaner", ver, "macos-arm64", release_tag)}"') - lines.append(f' sha256 "{sha(sha_map, "clang-include-cleaner", ver, "macos-arm64")}"') - lines.append(" end") - lines.append("") + for tool in resource_tools: + lines.append(f' resource "{tool}" do') + lines.append(f' url "{rel_url(tool, ver, platform, release_tag)}"') + lines.append(f' sha256 "{sha(sha_map, tool, ver, platform)}"') + lines.append(" end") + lines.append("") - lines.append(" end") - lines.append("") - - # ── Intel ── - lines.append(" on_intel do") - lines.append(f' url "{rel_url("clang-format", ver, "macos-amd64", release_tag)}"') - lines.append(f' sha256 "{sha(sha_map, "clang-format", ver, "macos-amd64")}"') - lines.append("") - - for tool in ["clang-tidy", "clang-query", "clang-apply-replacements"]: - lines.append(f' resource "{tool}" do') - lines.append(f' url "{rel_url(tool, ver, "macos-amd64", release_tag)}"') - lines.append(f' sha256 "{sha(sha_map, tool, ver, "macos-amd64")}"') - lines.append(" end") - lines.append("") - - if include_cleaner: - lines.append(' resource "clang-include-cleaner" do') - lines.append(f' url "{rel_url("clang-include-cleaner", ver, "macos-amd64", release_tag)}"') - lines.append(f' sha256 "{sha(sha_map, "clang-include-cleaner", ver, "macos-amd64")}"') - lines.append(" end") + lines.append(" end") lines.append("") - lines.append(" end") - lines.append("") - # ── Install method ── + install_list = " ".join(resource_tools) lines.append(" def install") lines.append(' # Install the main binary (clang-format)') lines.append(' bin.install Dir["clang-format-*"].first => "clang-format"') lines.append("") lines.append(" # Install tool resources") - lines.append(" %w[clang-tidy clang-query clang-apply-replacements clang-include-cleaner].each do |tool|") + lines.append(f" %w[{install_list}].each do |tool|") lines.append(" next unless resource(tool)") lines.append(" resource(tool).stage do") lines.append(' bin.install Dir["*"].first => tool') @@ -192,11 +203,11 @@ def generate_bundle_formula( lines.append("") # ── Test ── + # clang-include-cleaner is intentionally not smoke-tested (it is version-gated). + test_tools = ["clang-format"] + [t for t in resource_tools if t != "clang-include-cleaner"] lines.append(" test do") - lines.append(' system "#{bin}/clang-format", "--version"') - lines.append(' system "#{bin}/clang-tidy", "--version"') - lines.append(' system "#{bin}/clang-query", "--version"') - lines.append(' system "#{bin}/clang-apply-replacements", "--version"') + for tool in test_tools: + lines.append(f' system "#{{bin}}/{tool}", "--version"') lines.append(" end") lines.append("end") lines.append("") @@ -277,7 +288,7 @@ def main() -> None: # All tools that may exist per version for ver in VERSIONS: - all_tools_for_ver = TOOLS_ALL + (TOOLS_IC if ver >= 18 else []) + all_tools_for_ver = TOOLS_ALL + (TOOLS_IC if ver >= 18 else []) + TOOLS_EXTRA for tool in all_tools_for_ver: for platform in PLATFORMS: name = asset_name(tool, ver, platform) From 4184e4fac8d9140b37c3c4eb68b2af85b9d4f2ae Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Sat, 4 Jul 2026 12:21:54 +0300 Subject: [PATCH 2/2] refactor(script): unify tools into a single list with per-tool min_version Collapse TOOLS_ALL / TOOLS_IC / TOOLS_EXTRA and the parallel INDIVIDUAL_TOOLS dict into one ordered TOOLS list. Version availability (clang-include-cleaner is LLVM 18+) is now a per-tool `min_version` attribute instead of a separate list, and the individual-formula class names are derived from the tool name (Homebrew's own convention) rather than duplicated. Behaviour is unchanged: the smoke-test set is now defined as "tools with no min_version" (always present on every version), which reproduces the existing test blocks, and generating against the current release still yields no diff against the hand-maintained Formula/*.rb. --- scripts/update-formula.py | 177 ++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 103 deletions(-) diff --git a/scripts/update-formula.py b/scripts/update-formula.py index f9d8be7..5cae3ce 100644 --- a/scripts/update-formula.py +++ b/scripts/update-formula.py @@ -24,75 +24,68 @@ from pathlib import Path -# ── Configuration ────────────────────────────────────────────────────────────── +# ── Configuration ───────────────────────────────────────────────────────────── OWNER = "cpp-linter" REPO = "clang-tools-static-binaries" VERSIONS = [22, 21, 20, 19, 18] -TOOLS_ALL = ["clang-format", "clang-tidy", "clang-query", "clang-apply-replacements"] -TOOLS_IC = ["clang-include-cleaner"] # LLVM 18+ -# LLVM utility binaries + clang-scan-deps, shipped for every supported version. -# These live alongside the clang tools in each upstream release and must be kept -# in sync here, otherwise a regeneration would silently drop them. -TOOLS_EXTRA = ["llvm-cov", "llvm-profdata", "llvm-symbolizer", "clang-scan-deps"] -PLATFORMS = ["macos-arm64", "macos-amd64"] -# Individual tool formula metadata -# tool_key -> {class_prefix, bin_name, desc} -INDIVIDUAL_TOOLS = { - "clang-format": { - "class_prefix": "ClangFormat", - "bin": "clang-format", - "desc": "clang-format", - }, - "clang-tidy": { - "class_prefix": "ClangTidy", - "bin": "clang-tidy", - "desc": "clang-tidy", - }, - "clang-query": { - "class_prefix": "ClangQuery", - "bin": "clang-query", - "desc": "clang-query", - }, - "clang-apply-replacements": { - "class_prefix": "ClangApplyReplacements", - "bin": "clang-apply-replacements", - "desc": "clang-apply-replacements", - }, - "clang-include-cleaner": { - "class_prefix": "ClangIncludeCleaner", - "bin": "clang-include-cleaner", - "desc": "clang-include-cleaner", - }, - "llvm-cov": { - "class_prefix": "LlvmCov", - "bin": "llvm-cov", - "desc": "llvm-cov", - }, - "llvm-profdata": { - "class_prefix": "LlvmProfdata", - "bin": "llvm-profdata", - "desc": "llvm-profdata", - }, - "llvm-symbolizer": { - "class_prefix": "LlvmSymbolizer", - "bin": "llvm-symbolizer", - "desc": "llvm-symbolizer", - }, - "clang-scan-deps": { - "class_prefix": "ClangScanDeps", - "bin": "clang-scan-deps", - "desc": "clang-scan-deps", - }, -} +# Every tool shipped in an upstream release, in the order they appear in the +# generated formulae. `min_version` gates a tool to the LLVM releases that +# actually ship it (e.g. clang-include-cleaner only exists from LLVM 18); tools +# without it are assumed present on every supported version. +TOOLS = [ + {"name": "clang-format"}, + {"name": "clang-tidy"}, + {"name": "clang-query"}, + {"name": "clang-apply-replacements"}, + {"name": "clang-include-cleaner", "min_version": 18}, + {"name": "llvm-cov"}, + {"name": "llvm-profdata"}, + {"name": "llvm-symbolizer"}, + {"name": "clang-scan-deps"}, +] + +# The bundle formula installs clang-format as its primary binary; every other +# tool is attached as a Homebrew `resource`. +PRIMARY_TOOL = "clang-format" + +PLATFORMS = ["macos-arm64", "macos-amd64"] SCRIPT_DIR = Path(__file__).resolve().parent TAP_DIR = SCRIPT_DIR.parent FORMULA_DIR = TAP_DIR / "Formula" -# ── Helpers ──────────────────────────────────────────────────────────────────── +# ── Tool helpers ──────────────────────────────────────────────────────────── + + +def tools_for_version(version: int) -> list[str]: + """Names of the tools shipped for a given LLVM version, in formula order.""" + return [t["name"] for t in TOOLS if version >= t.get("min_version", 0)] + + +def always_available_tools() -> list[str]: + """Tools present on every supported version (no `min_version`). + + These are the ones safe to smoke-test in the bundle formula; version-gated + tools are installed with a `rescue` and skipped in the test to keep it valid + across all versions. + """ + return [t["name"] for t in TOOLS if "min_version" not in t] + + +def formula_class(name: str, version: int | None = None, is_latest: bool = True) -> str: + """Homebrew formula class name for a tool (e.g. llvm-cov -> LlvmCov). + + Follows Homebrew's own convention: split on '-', capitalize each part, join. + Versioned formulae get an `ATxx` suffix (e.g. clang-tidy@18 -> ClangTidyAT18). + """ + base = "".join(part.capitalize() for part in name.split("-")) + return base if is_latest else f"{base}AT{version}" + + +# ── Checksum helpers ─────────────────────────────────────────────────────── def sha256_of_url(url: str) -> str | None: @@ -126,20 +119,6 @@ def rel_url(tool: str, version: int, platform: str, release_tag: str) -> str: return asset_url(tool, version, platform, release_tag) -def bundle_resource_tools(version: int) -> list[str]: - """Tools bundled as Homebrew resources (everything except clang-format), in file order. - - clang-format is the primary download (url/sha on the formula body); every other - tool is emitted as a `resource` block. Order here defines the order in the - generated formula and must match the hand-maintained files. - """ - tools = ["clang-tidy", "clang-query", "clang-apply-replacements"] - if version >= 18: - tools.append("clang-include-cleaner") - tools += TOOLS_EXTRA - return tools - - # ── Bundle formula (clang-tools) ─────────────────────────────────────────────── @@ -153,11 +132,11 @@ def generate_bundle_formula( ver = version class_name = "ClangTools" if is_latest else f"ClangToolsAT{ver}" - resource_tools = bundle_resource_tools(ver) + available = tools_for_version(ver) + resource_tools = [t for t in available if t != PRIMARY_TOOL] - # Human-readable description lists clang-format first, then every resource. - desc_tools = ["clang-format"] + resource_tools - tools_desc = ", ".join(desc_tools[:-1]) + ", and " + desc_tools[-1] + # Human-readable description lists every shipped tool, primary first. + tools_desc = ", ".join(available[:-1]) + ", and " + available[-1] lines: list[str] = [] @@ -170,8 +149,8 @@ def generate_bundle_formula( # ── ARM / Intel share the same resource layout, differing only by platform ── for on_block, platform in (("on_arm", "macos-arm64"), ("on_intel", "macos-amd64")): lines.append(f" {on_block} do") - lines.append(f' url "{rel_url("clang-format", ver, platform, release_tag)}"') - lines.append(f' sha256 "{sha(sha_map, "clang-format", ver, platform)}"') + lines.append(f' url "{rel_url(PRIMARY_TOOL, ver, platform, release_tag)}"') + lines.append(f' sha256 "{sha(sha_map, PRIMARY_TOOL, ver, platform)}"') lines.append("") for tool in resource_tools: @@ -203,8 +182,9 @@ def generate_bundle_formula( lines.append("") # ── Test ── - # clang-include-cleaner is intentionally not smoke-tested (it is version-gated). - test_tools = ["clang-format"] + [t for t in resource_tools if t != "clang-include-cleaner"] + # Only smoke-test tools guaranteed present on every version; version-gated + # tools may be absent and are skipped to keep the test valid across versions. + test_tools = [t for t in always_available_tools() if t in available] lines.append(" test do") for tool in test_tools: lines.append(f' system "#{{bin}}/{tool}", "--version"') @@ -219,18 +199,16 @@ def generate_bundle_formula( def generate_individual_formula( - tool_key: str, + tool: str, version: int, release_tag: str, sha_map: dict[str, str], is_latest: bool, ) -> str: """Generate the content of an individual tool formula.""" - info = INDIVIDUAL_TOOLS[tool_key] ver = version - class_name = info["class_prefix"] if is_latest else f'{info["class_prefix"]}AT{ver}' - bin_name = info["bin"] - desc = f"Static binary for {info['desc']}" + class_name = formula_class(tool, ver, is_latest) + desc = f"Static binary for {tool}" lines: list[str] = [] @@ -242,27 +220,27 @@ def generate_individual_formula( # ── ARM ── lines.append(" on_arm do") - lines.append(f' url "{rel_url(tool_key, ver, "macos-arm64", release_tag)}"') - lines.append(f' sha256 "{sha(sha_map, tool_key, ver, "macos-arm64")}"') + lines.append(f' url "{rel_url(tool, ver, "macos-arm64", release_tag)}"') + lines.append(f' sha256 "{sha(sha_map, tool, ver, "macos-arm64")}"') lines.append(" end") lines.append("") # ── Intel ── lines.append(" on_intel do") - lines.append(f' url "{rel_url(tool_key, ver, "macos-amd64", release_tag)}"') - lines.append(f' sha256 "{sha(sha_map, tool_key, ver, "macos-amd64")}"') + lines.append(f' url "{rel_url(tool, ver, "macos-amd64", release_tag)}"') + lines.append(f' sha256 "{sha(sha_map, tool, ver, "macos-amd64")}"') lines.append(" end") lines.append("") # ── Install method ── lines.append(" def install") - lines.append(f' bin.install Dir["{tool_key}-*"].first => "{bin_name}"') + lines.append(f' bin.install Dir["{tool}-*"].first => "{tool}"') lines.append(" end") lines.append("") # ── Test ── lines.append(" test do") - lines.append(f' system "#{{bin}}/{bin_name}", "--version"') + lines.append(f' system "#{{bin}}/{tool}", "--version"') lines.append(" end") lines.append("end") lines.append("") @@ -283,13 +261,11 @@ def main() -> None: print(f":: Fetching checksums from release {release_tag} ...") - # Collect SHA-256 for every asset + # Collect SHA-256 for every asset shipped by each version. sha_map: dict[str, str] = {} - # All tools that may exist per version for ver in VERSIONS: - all_tools_for_ver = TOOLS_ALL + (TOOLS_IC if ver >= 18 else []) + TOOLS_EXTRA - for tool in all_tools_for_ver: + for tool in tools_for_version(ver): for platform in PLATFORMS: name = asset_name(tool, ver, platform) url = asset_url(tool, ver, platform, release_tag) @@ -319,16 +295,11 @@ def main() -> None: for i, ver in enumerate(VERSIONS): is_latest = (i == 0) - all_tools_for_ver = list(INDIVIDUAL_TOOLS.keys()) - # clang-include-cleaner only from LLVM 18+ - if ver < 18: - all_tools_for_ver = [t for t in all_tools_for_ver if t != "clang-include-cleaner"] - - for tool_key in all_tools_for_ver: - formula_name = f"{tool_key}.rb" if is_latest else f"{tool_key}@{ver}.rb" + for tool in tools_for_version(ver): + formula_name = f"{tool}.rb" if is_latest else f"{tool}@{ver}.rb" formula_path = FORMULA_DIR / formula_name - content = generate_individual_formula(tool_key, ver, release_tag, sha_map, is_latest) + content = generate_individual_formula(tool, ver, release_tag, sha_map, is_latest) formula_path.write_text(content) print(f" ✓ {formula_name}")