Detect tool vendor by query, not by name
When to use
You are writing code that branches on which compiler, linker,
interpreter, or other build tool is in use — GCC vs. Clang, CPython
vs. PyPy, BSD vs. GNU coreutils, system Python vs. a pyenv shim — and
your current approach is to look at the executable's name or path
("gcc" in argv0, which python3, basename $CC).
Signal phrases: "we assume /usr/bin/gcc is GCC", "the driver is
called cc so it must be…", "python3 on PATH is 3.x", or a bug
report that reproduces only on macOS / only on a system with a
vendor-renamed toolchain.
Problem
Tool names lie. Common traps:
- macOS ships
/usr/bin/gcc but it's Apple Clang under the
hood. Any substring check like 'gcc' in cc_path misclassifies
it as GNU and then tries to pass GNU-only flags
(-static-libgcc, -flto=auto, -fno-fat-lto-objects, …),
which Clang rejects at link time.
cc is a symlink whose target varies by distro: GCC on
most Linux, Clang on FreeBSD / macOS, tcc on some minimal
installs. The name cc tells you nothing.
python3 on PATH may be the system's frozen 3.9, a pyenv
shim, a Homebrew keg, or a virtualenv. Checking the path
(/usr/bin/python3 → "system Python") gives you the source,
not the version.
- GNU vs BSD coreutils —
sed, awk, tar, date on
macOS are BSD variants with different flag sets; they're
spelled exactly the same as their GNU cousins.
- Cross-compilers and wrappers (
ccache gcc, distcc clang,
arm-linux-gnueabihf-gcc) contain the vendor substring but
may forward to a different backend than the name implies.
The fix is always the same: ask the tool.
Solution
Run the tool's identity query once, at init time, and cache
the result. Don't re-probe on every call site.
Parse a known-stable field, not the whole banner:
- C/C++ drivers: first line of
cc --version. Look for
'Apple clang' / 'clang' / 'gcc' / 'Free Software Foundation'. Normalize to a closed enum
({'clang', 'gcc', 'unknown'}).
- Python:
python -c 'import sys; print(sys.version_info[:2])'
or python -V and parse "Python X.Y.Z". Compare as a
tuple, never as a float (3.10 < 3.9 if you use floats).
- coreutils:
sed --version 2>&1 | head -1 → "GNU sed" vs.
"invalid option" on BSD.
Have a defined unknown bucket. Exotic toolchains exist;
don't let the detector crash on them. Emit a warning and fall
back to the most conservative flag set.
Gate feature flags on the cached vendor, not on the path:
# Wrong
if 'gcc' in self.cc:
flags.append('-static-libgcc')
# Right
if self.cc_vendor == 'gcc':
flags.append('-static-libgcc')
Let the user override, because detection has its limits.
Honor an env var ($CC_VENDOR, $BLADE_PYTHON_INTERPRETER,
$FORCE_GNU_SED) before auto-detection, so users on exotic
toolchains can opt out without patching the detector.
Example
Real case from blade-build (commit b5e09dd). The old detector:
# Misclassifies macOS /usr/bin/gcc (= Apple Clang) as GCC.
def cc_is(self, vendor: str) -> bool:
return vendor in self.cc # substring match on the binary path
On macOS, self.cc == '/usr/bin/gcc', so cc_is('gcc') returns
True, so the link step adds -static-libgcc, so the linker
errors out (Apple Clang doesn't know that flag).
The fix — probe once, cache, compare exactly:
class ToolChain:
def __init__(self, ...):
self._cc_vendor = self._probe_cc_vendor(self.cc)
@staticmethod
def _probe_cc_vendor(cc: str) -> str:
out, _ = run_command([cc, '--version'])
first = out.splitlines()[0].lower() if out else ''
if 'clang' in first: # covers 'Apple clang' too
return 'clang'
if 'gcc' in first or 'free software foundation' in first:
return 'gcc'
return 'unknown'
def cc_is(self, vendor: str) -> bool:
return self._cc_vendor == vendor # exact equality
Analogous launcher fix for Python interpreter selection: instead of
"python3 is on PATH, done", _probe_python() runs
<candidate> -V, parses the version tuple, and accepts only
>= 3.10 — and it consults $BLADE_PYTHON_INTERPRETER first so
users whose system python3 is 3.9 can override with 3.12.
Pitfalls
- Don't parse localized output. Some tools localize
--version
(Paquet GNU sed version ...). Set LC_ALL=C in the probe
command, or match on the program name token that does not get
translated (sed, gcc).
- Don't probe inside hot paths.
--version forks a process;
calling it from every compile rule will dominate build time.
Probe once at toolchain construction and cache.
- Don't rely on exit code alone. Some tools exit non-zero for
--version (very rare, but bc and a few BSD tools do). Also
accept stderr; some write the banner there.
- Watch out for "wrapper noise":
ccache gcc --version prints
the real compiler's banner, which is what you want; but a
user-written wrapper script might swallow --version. Document
that the env-var override is the escape hatch.
- A closed enum beats free-form strings. Returning the raw
banner to call sites invites every caller to re-parse it and
disagree about edge cases. Collapse to
{'gcc', 'clang', 'msvc', 'unknown'} at the boundary.
- Path-based checks are still OK for locating the tool (did
the user install it? which one is first on PATH?), but never
for deciding what it is.
See also
1---2name: detect-tool-vendor-by-query3description: Identify a tool's real vendor/version by asking it (`--version`), not by sniffing its filename or PATH entry.4---56# Detect tool vendor by query, not by name78## When to use910You are writing code that branches on *which* compiler, linker,11interpreter, or other build tool is in use — GCC vs. Clang, CPython12vs. PyPy, BSD vs. GNU coreutils, system Python vs. a pyenv shim — and13your current approach is to look at the executable's **name or path**14(`"gcc" in argv0`, `which python3`, `basename $CC`).1516Signal phrases: "we assume `/usr/bin/gcc` is GCC", "the driver is17called `cc` so it must be…", "`python3` on PATH is 3.x", or a bug18report that reproduces only on macOS / only on a system with a19vendor-renamed toolchain.2021## Problem2223Tool names lie. Common traps:2425- **macOS ships `/usr/bin/gcc`** but it's Apple Clang under the26 hood. Any substring check like `'gcc' in cc_path` misclassifies27 it as GNU and then tries to pass GNU-only flags28 (`-static-libgcc`, `-flto=auto`, `-fno-fat-lto-objects`, …),29 which Clang rejects at link time.30- **`cc` is a symlink** whose target varies by distro: GCC on31 most Linux, Clang on FreeBSD / macOS, tcc on some minimal32 installs. The name `cc` tells you nothing.33- **`python3` on PATH** may be the system's frozen 3.9, a pyenv34 shim, a Homebrew keg, or a virtualenv. Checking the path35 (`/usr/bin/python3` → "system Python") gives you the *source*,36 not the *version*.37- **GNU vs BSD coreutils** — `sed`, `awk`, `tar`, `date` on38 macOS are BSD variants with different flag sets; they're39 spelled exactly the same as their GNU cousins.40- **Cross-compilers and wrappers** (`ccache gcc`, `distcc clang`,41 `arm-linux-gnueabihf-gcc`) contain the vendor substring but42 may forward to a different backend than the name implies.4344The fix is always the same: **ask the tool**.4546## Solution47481. **Run the tool's identity query once, at init time**, and cache49 the result. Don't re-probe on every call site.502. **Parse a known-stable field**, not the whole banner:51 - C/C++ drivers: first line of `cc --version`. Look for52 `'Apple clang'` / `'clang'` / `'gcc'` / `'Free Software53 Foundation'`. Normalize to a closed enum54 (`{'clang', 'gcc', 'unknown'}`).55 - Python: `python -c 'import sys; print(sys.version_info[:2])'`56 or `python -V` and parse `"Python X.Y.Z"`. Compare as a57 tuple, never as a float (`3.10 < 3.9` if you use floats).58 - coreutils: `sed --version 2>&1 | head -1` → `"GNU sed"` vs.59 "invalid option" on BSD.603. **Have a defined `unknown` bucket.** Exotic toolchains exist;61 don't let the detector crash on them. Emit a warning and fall62 back to the most conservative flag set.634. **Gate feature flags on the cached vendor**, not on the path:6465 ```python66 # Wrong67 if 'gcc' in self.cc:68 flags.append('-static-libgcc')6970 # Right71 if self.cc_vendor == 'gcc':72 flags.append('-static-libgcc')73 ```74755. **Let the user override**, because detection has its limits.76 Honor an env var (`$CC_VENDOR`, `$BLADE_PYTHON_INTERPRETER`,77 `$FORCE_GNU_SED`) before auto-detection, so users on exotic78 toolchains can opt out without patching the detector.7980## Example8182Real case from blade-build (commit `b5e09dd`). The old detector:8384```python85# Misclassifies macOS /usr/bin/gcc (= Apple Clang) as GCC.86def cc_is(self, vendor: str) -> bool:87 return vendor in self.cc # substring match on the binary path88```8990On macOS, `self.cc == '/usr/bin/gcc'`, so `cc_is('gcc')` returns91`True`, so the link step adds `-static-libgcc`, so the linker92errors out (Apple Clang doesn't know that flag).9394The fix — probe once, cache, compare exactly:9596```python97class ToolChain:98 def __init__(self, ...):99 self._cc_vendor = self._probe_cc_vendor(self.cc)100101 @staticmethod102 def _probe_cc_vendor(cc: str) -> str:103 out, _ = run_command([cc, '--version'])104 first = out.splitlines()[0].lower() if out else ''105 if 'clang' in first: # covers 'Apple clang' too106 return 'clang'107 if 'gcc' in first or 'free software foundation' in first:108 return 'gcc'109 return 'unknown'110111 def cc_is(self, vendor: str) -> bool:112 return self._cc_vendor == vendor # exact equality113```114115Analogous launcher fix for Python interpreter selection: instead of116"`python3` is on PATH, done", `_probe_python()` runs117`<candidate> -V`, parses the version tuple, and accepts only118`>= 3.10` — and it consults `$BLADE_PYTHON_INTERPRETER` first so119users whose system `python3` is 3.9 can override with `3.12`.120121## Pitfalls122123- **Don't parse localized output.** Some tools localize `--version`124 (`Paquet GNU sed version ...`). Set `LC_ALL=C` in the probe125 command, or match on the program name token that does not get126 translated (`sed`, `gcc`).127- **Don't probe inside hot paths.** `--version` forks a process;128 calling it from every compile rule will dominate build time.129 Probe once at toolchain construction and cache.130- **Don't rely on exit code alone.** Some tools exit non-zero for131 `--version` (very rare, but `bc` and a few BSD tools do). Also132 accept stderr; some write the banner there.133- **Watch out for "wrapper noise"**: `ccache gcc --version` prints134 the real compiler's banner, which is what you want; but a135 user-written wrapper script might swallow `--version`. Document136 that the env-var override is the escape hatch.137- **A closed enum beats free-form strings.** Returning the raw138 banner to call sites invites every caller to re-parse it and139 disagree about edge cases. Collapse to140 `{'gcc', 'clang', 'msvc', 'unknown'}` at the boundary.141- **Path-based checks are still OK for *locating* the tool** (did142 the user install it? which one is first on PATH?), but never143 for deciding *what it is*.144145## See also146147- [workspace-path-constraints](../workspace-path-constraints/SKILL.md)148- [python-code-audit-sweep](../python-code-audit-sweep/SKILL.md)149- [test-layout-evolution](../test-layout-evolution/SKILL.md)