Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 8 additions & 46 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
{'platform': 'linux-amd64', 'os': 'linux', 'arch': 'amd64', 'runner': 'ubuntu-22.04'},
{'platform': 'linux-arm64', 'os': 'linux', 'arch': 'arm64', 'runner': 'ubuntu-22.04-arm'},
{'platform': 'macos-amd64', 'os': 'macos', 'arch': 'amd64', 'runner': 'macos-15-intel'},
{'platform': 'macos-arm64', 'os': 'macos', 'arch': 'arm64', 'runner': 'macos-latest'},
{'platform': 'macos-arm64', 'os': 'macos', 'arch': 'arm64', 'runner': 'macos-14'},
{'platform': 'windows-amd64', 'os': 'windows', 'arch': 'amd64', 'runner': 'windows-latest'},
{'platform': 'windows-arm64', 'os': 'windows', 'arch': 'arm64', 'runner': 'windows-11-arm'},
]
Expand Down Expand Up @@ -71,16 +71,20 @@ jobs:
# We download a tarball of this repo, as the presence of a .git directory leaks
# The commit hash of this repository into the clang binaries
shell: bash
run: curl -L https://github.com/${{ github.repository }}/archive/${{ github.ref }}.tar.gz | tar xvz --strip 1
run: curl -sL https://github.com/${{ github.repository }}/archive/${{ github.ref }}.tar.gz | tar xvz --strip 1
- name: Build
shell: bash
run: python3 build.py --version "${{ matrix.clang-version }}" --platform ${{ matrix.platform }}
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: clang-tools-${{ matrix.release }}-${{ env.suffix }}
path: "${{ matrix.release }}/build/**/clang-*-${{ env.suffix }}*"
path: |
${{ matrix.release }}/build/**/clang-*-${{ env.suffix }}*
${{ matrix.release }}/build/**/llvm-*-${{ env.suffix }}*
# ** covers both bin/ (Linux/macOS) and MinSizeRel/bin/ (Windows)
# clang-* matches clang-format, clang-tidy, clang-query, clang-scan-deps, etc.
# llvm-* matches llvm-cov, llvm-profdata, llvm-symbolizer
retention-days: 3
test-release:
permissions: {}
Expand All @@ -98,7 +102,7 @@ jobs:
runner: macos-15-intel
- os: macos
arch: arm64
runner: macos-latest
runner: macos-14
- os: windows
arch: amd64
runner: windows-latest
Expand All @@ -118,52 +122,10 @@ jobs:
- name: List files (Linux, macOS)
if: ${{ matrix.os == 'linux' || matrix.os == 'macos' }}
run: ls -laR artifacts/
- name: Smoke test each clang tool (Linux, macOS)
if: ${{ matrix.os == 'linux' || matrix.os == 'macos' }}
run: |
cd artifacts
# From the artifacts directory, loop over each executable
# (not .sha512sum files) and
# invoke the --version command to verify

for tool in $(find . -type f); do
# Skip the sha512sum files
if [[ $tool == *.sha512sum ]]; then
continue
fi
chmod +x $tool
# Run the tool with --version and print the output
echo "Running $tool --version"
$tool --version
done
- name: List files (Windows)
if: ${{ matrix.os == 'windows' }}
run: |
Get-ChildItem -Recurse artifacts | Format-List

- name: Smoke test each clang tool (Windows)
if: ${{ matrix.os == 'windows' }}
shell: pwsh
run: |
Set-Location artifacts

# Find all files excluding *.sha512sum
$tools = Get-ChildItem -Recurse -File | Where-Object { $_.Name -notlike '*.sha512sum' }

foreach ($tool in $tools) {
# Ensure the file is executable
$toolPath = $tool.FullName

# Print which tool is being run
Write-Host "Running $toolPath --version"

try {
# Attempt to run the tool with --version
& $toolPath --version
} catch {
Write-Host "Failed to run $toolPath --version. Error: $_"
}
}
draft-release:
if: github.event_name != 'pull_request'
permissions:
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing

Thanks for your interest in contributing! This project builds and distributes static binaries of clang tools (clang-format, clang-tidy, clang-query, clang-apply-replacements, clang-include-cleaner) for multiple platforms.
Thanks for your interest in contributing! This project builds and distributes static binaries of clang/LLVM tools (clang-format, clang-tidy, clang-query, clang-apply-replacements, clang-include-cleaner, llvm-cov, llvm-profdata, llvm-symbolizer, clang-scan-deps) for multiple platforms.

## Quick Start

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![homebrew](https://img.shields.io/badge/homebrew-tap-FBB040?logo=homebrew&logoColor=white)](https://github.com/cpp-linter/homebrew-tap)
[![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/)

Includes **[clang-format](https://clang.llvm.org/docs/ClangFormat.html), [clang-tidy](https://clang.llvm.org/extra/clang-tidy/), [clang-query](https://github.com/llvm/llvm-project/tree/main/clang-tools-extra/clang-query), [clang-apply-replacements](https://github.com/llvm/llvm-project/tree/main/clang-tools-extra/clang-apply-replacements) and [clang-include-cleaner](https://clang.llvm.org/extra/clang-tidy/checks/misc/include-cleaner.html)** (LLVM 18+).
Includes **clang-format, clang-tidy, clang-query, clang-apply-replacements, clang-include-cleaner** (LLVM 18+), **llvm-cov, llvm-profdata, llvm-symbolizer and clang-scan-deps** — all the tools you need for C/C++ formatting, static analysis, coverage, symbolization and dependency scanning.

## Table of Contents

Expand Down Expand Up @@ -113,7 +113,7 @@ Or download pre-built binaries directly from the [Releases](https://github.com/c

- Download clang-tools static binaries for your platform from the [Releases](https://github.com/cpp-linter/clang-tools-static-binaries/releases) tab.
- Alternatively, use [pip](https://github.com/cpp-linter/clang-tools-pip), [asdf](https://github.com/cpp-linter/asdf-clang-tools), or [Homebrew](https://github.com/cpp-linter/homebrew-tap) (macOS) to download and manage them.
- For programmatic access, the latest release includes a [`versions.json`](https://github.com/cpp-linter/clang-tools-static-binaries/releases/latest/download/versions.json) file that maps each clang tool version to its LLVM source release (e.g., `{"18": "llvm-project-18.1.5.src"}`). This is a stable machine-readable entry point for scripts and downstream tools.
- For programmatic access, the latest release includes a [`versions.json`](https://github.com/cpp-linter/clang-tools-static-binaries/releases/latest/download/versions.json) file that maps each LLVM version to its source release, lists all shipped tools (with minimum-version constraints), and enumerates supported platforms. This is the **single source of truth** for all downstream channels (pip, asdf, homebrew, scoop, etc.) — do not maintain separate tool/version lists elsewhere.

## How can I trust this repository?

Expand Down
52 changes: 36 additions & 16 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,32 @@ def _load_releases() -> dict[str, str]:
"clang-tidy",
"clang-apply-replacements",
"clang-include-cleaner", # available starting LLVM 18
"llvm-cov",
"llvm-profdata",
"llvm-symbolizer",
"clang-scan-deps",
]

# Minimum LLVM major version that includes clang-include-cleaner as a standalone binary.
# Minimum LLVM major version for tools that were introduced after LLVM 11.
INCLUDE_CLEANER_MIN_VERSION = 18
CLANG_SCAN_DEPS_MIN_VERSION = 12


def active_tools(version: str) -> list[str]:
"""Return the list of tools that are buildable for *version*.

clang-include-cleaner was introduced as a standalone tool in LLVM 18.
Earlier versions only had it as a library, not a build target.

clang-scan-deps became available as a standalone tool in LLVM 12.
Earlier versions (11) only had it as an experimental library.
"""
tools = list(TOOLS)
major = int(version.split(".")[0])
if major < INCLUDE_CLEANER_MIN_VERSION:
tools.remove("clang-include-cleaner")
if major < CLANG_SCAN_DEPS_MIN_VERSION:
tools.remove("clang-scan-deps")
return tools


Expand Down Expand Up @@ -116,27 +126,14 @@ def sha512_file(path: Path) -> str:


def download_file(url: str, dest: Path) -> None:
"""Download *url* to *dest* with a simple progress indicator."""
"""Download *url* to *dest*."""
if dest.exists():
print(f"[skip] {dest.name} already downloaded.")
return
print(f"Downloading {url} ...", flush=True)
tmp = dest.with_suffix(".tmp")
try:
with urllib.request.urlopen(url) as resp, open(tmp, "wb") as fh:
total = int(resp.headers.get("Content-Length", 0))
downloaded = 0
block = 1 << 16
while True:
data = resp.read(block)
if not data:
break
fh.write(data)
downloaded += len(data)
if total:
pct = downloaded * 100 // total
print(f"\r {pct:3d}%", end="", flush=True)
print()
urllib.request.urlretrieve(url, tmp)
tmp.rename(dest)
except Exception:
tmp.unlink(missing_ok=True)
Expand Down Expand Up @@ -405,9 +402,32 @@ def build(version: str, target_platform: str, script_dir: Path) -> None:
# 6. Smoke test
# ------------------------------------------------------------------
bins = bin_dir(release, is_windows)

# Basic --version smoke test for every built tool.
# Note: llvm-profdata on LLVM < 17 uses a subcommand interface and
# does NOT support --version. See llvm-profdata.cpp main() — LLVM 17
# added explicit `if (strcmp(argv[1], "--version") == 0)` handling.
# Older versions only recognise subcommands (merge/show/overlap) and
# --help. We verify the binary is executable by running it with no
# args and checking for the expected usage output.
llvm_major = int(version.split(".")[0])
for tool in tools:
exe = bins / f"{tool}{dot_exe}"
print(f"\nSmoke-testing {exe} ...")
if tool == "llvm-profdata" and llvm_major < 17:
result = subprocess.run(
[str(exe)],
capture_output=True,
text=True,
)
if "USAGE" not in result.stdout and "USAGE" not in result.stderr:
raise RuntimeError(
f"{exe.name} did not produce expected usage output:\n"
f" stdout: {result.stdout.strip() or '(empty)'}\n"
f" stderr: {result.stderr.strip() or '(empty)'}"
)
print(" Binary OK (subcommand interface)")
continue
run([str(exe), "--version"])

# ------------------------------------------------------------------
Expand Down
32 changes: 29 additions & 3 deletions release.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,45 @@
def generate_versions_json(tag: str, output_dir: str = ".") -> Path:
"""Write ``versions.json`` to *output_dir* and return its path.

The generated JSON contains the build timestamp, release tag, and a
The generated JSON contains the build timestamp, release tag, a
mapping of LLVM release names (keys) to source tarball identifiers
(values).
(values), the full list of shipped tools (with minimum LLVM version
constraints), and the supported platforms. This is the single source
of truth for all downstream channels (pip, asdf, homebrew, scoop, etc.).
"""
all_tools = build.TOOLS
tools_info: dict[str, dict] = {}
for tool in all_tools:
info: dict[str, object] = {}
if tool == "clang-include-cleaner":
info["min_llvm_version"] = build.INCLUDE_CLEANER_MIN_VERSION
if tool == "clang-scan-deps":
info["min_llvm_version"] = build.CLANG_SCAN_DEPS_MIN_VERSION
tools_info[tool] = info

platforms = [
"linux-amd64",
"linux-arm64",
"macos-amd64",
"macos-arm64",
"windows-amd64",
"windows-arm64",
]

data = {
"built_at": datetime.now(timezone.utc).isoformat(),
"release_tag": tag,
"llvm_versions": build.RELEASES,
"tools": tools_info,
"platforms": platforms,
}
out_path = Path(output_dir) / "versions.json"
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
print(f"Created {out_path} ({len(build.RELEASES)} versions)")
print(
f"Created {out_path} ({len(build.RELEASES)} versions, "
f"{len(tools_info)} tools, {len(platforms)} platforms)"
)
return out_path


Expand Down