|
| 1 | +defmodule Mix.Tasks.Clang.Tidy do |
| 2 | + @moduledoc """ |
| 3 | + Run clang-tidy over the C++ NIF sources in `c_src/`. |
| 4 | +
|
| 5 | + Unlike `make cppcheck`, clang-tidy compiles each translation unit, so it |
| 6 | + needs the MLX / Fine / ERTS headers and the exact build flags. This task |
| 7 | + supplies the same env `elixir_make` uses when building the NIF — reusing |
| 8 | + the already-built (cached) MLX rather than fetching a second copy — and |
| 9 | + then invokes the `clang-tidy` Makefile target, which analyses the NIF |
| 10 | + sources with the build's `$(CXXFLAGS)`. |
| 11 | +
|
| 12 | + ## Usage |
| 13 | +
|
| 14 | + mix clang.tidy |
| 15 | +
|
| 16 | + Requires clang-tidy on `PATH` (`brew install llvm`); point at a specific |
| 17 | + binary with `CLANG_TIDY=/path/to/clang-tidy`. The enabled checks and the |
| 18 | + header filter live in the repo-root `.clang-tidy`. |
| 19 | + """ |
| 20 | + |
| 21 | + use Mix.Task |
| 22 | + |
| 23 | + @shortdoc "Run clang-tidy over the C++ NIF sources" |
| 24 | + |
| 25 | + @impl Mix.Task |
| 26 | + def run(_args) do |
| 27 | + # Ensure MLX is available (its headers are what clang-tidy parses). The |
| 28 | + # :emily_mlx compiler alias reuses an existing install and only builds |
| 29 | + # from source on a cold cache — same as `mix bench.native`. |
| 30 | + Mix.Task.run("compile.emily_mlx", []) |
| 31 | + |
| 32 | + # Ask the mix project for its make_env — the same map `elixir_make` |
| 33 | + # passes to `make` — and add ERTS_INCLUDE_DIR, which elixir_make sets |
| 34 | + # itself at build time (so make_env/0 omits it) but a standalone `make` |
| 35 | + # invocation does not get. |
| 36 | + env = |
| 37 | + Mix.Project.config() |
| 38 | + |> Keyword.fetch!(:make_env) |
| 39 | + |> case do |
| 40 | + f when is_function(f, 0) -> f.() |
| 41 | + m when is_map(m) -> m |
| 42 | + end |
| 43 | + |> Map.put("ERTS_INCLUDE_DIR", erts_include_dir()) |
| 44 | + |> Enum.to_list() |
| 45 | + |
| 46 | + make = System.get_env("MAKE") || "make" |
| 47 | + |
| 48 | + Mix.shell().info("Running: #{make} clang-tidy") |
| 49 | + |
| 50 | + {_out, status} = |
| 51 | + System.cmd(make, ["clang-tidy"], |
| 52 | + env: env, |
| 53 | + into: IO.stream(:stdio, :line), |
| 54 | + stderr_to_stdout: true |
| 55 | + ) |
| 56 | + |
| 57 | + if status != 0 do |
| 58 | + Mix.raise("clang.tidy reported findings (exit #{status})") |
| 59 | + end |
| 60 | + end |
| 61 | + |
| 62 | + # Mirror how elixir_make derives ERTS_INCLUDE_DIR: the Erlang headers |
| 63 | + # (erl_nif.h, reached through fine.hpp) live under the OTP install. Honour |
| 64 | + # an explicit override if one is already exported. |
| 65 | + defp erts_include_dir do |
| 66 | + System.get_env("ERTS_INCLUDE_DIR") || |
| 67 | + Path.join([ |
| 68 | + to_string(:code.root_dir()), |
| 69 | + "erts-#{:erlang.system_info(:version)}", |
| 70 | + "include" |
| 71 | + ]) |
| 72 | + end |
| 73 | +end |
0 commit comments