Skip to content

Commit f04e6c8

Browse files
committed
feat: detect missing clang/opt at startup; per-distro install hint
1 parent 2476c69 commit f04e6c8

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@ func main() {
592592
codegen.AnsiEnabled = true
593593
}
594594

595+
// Verify the LLVM toolchain (clang + opt) is available before any
596+
// path that needs it. Prints a per-distro install hint and exits
597+
// on miss; warm runs short-circuit via per-tool marker files.
598+
requireTools()
599+
595600
// Prime the disk-cached host-info so subsequent helpers (target
596601
// triple detection in codegen, version banner in cache keys, link
597602
// probe) never re-spawn clang on a warm machine.

tools.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package main
2+
3+
// Required-toolchain check.
4+
//
5+
// tin needs `clang` (frontend + C source compile) and `opt` (LLVM IR
6+
// optimizer / ThinLTO pre-link prep / coro split). Cross-compile also
7+
// needs `ld.lld`, but we defer that check to the cross-compile path so
8+
// native builds don't fail on a missing-but-unused tool.
9+
//
10+
// On the fast path we touch a marker file under .build/host-info/tools/
11+
// per detected tool. On subsequent invocations, presence of the marker
12+
// + presence of the tool in $PATH skips the more expensive --version
13+
// probe path. Negative results are NOT cached: a missing tool today may
14+
// be installed tomorrow.
15+
16+
import (
17+
"fmt"
18+
"os"
19+
"os/exec"
20+
"path/filepath"
21+
"runtime"
22+
"strings"
23+
)
24+
25+
// toolMarkerDir is where the per-tool presence markers live. Sits under
26+
// .build/host-info/ so a single `rm -rf .build/host-info` (or `tin
27+
// clean`) clears every cached probe at once.
28+
const toolMarkerDir = ".build/host-info/tools"
29+
30+
// requireTools verifies the toolchain tin needs to drive the backend.
31+
// On any miss, prints a per-distro install hint to stderr and exits.
32+
// Skipped on warm runs (marker file present + tool still in $PATH).
33+
func requireTools() {
34+
missing := []string{}
35+
36+
for _, name := range []string{"clang", "opt"} {
37+
if !toolAvailable(name) {
38+
missing = append(missing, name)
39+
}
40+
}
41+
42+
if len(missing) > 0 {
43+
printMissingToolsHint(missing)
44+
os.Exit(1)
45+
}
46+
}
47+
48+
// toolAvailable returns true when `name` is in $PATH. Touches a marker
49+
// under toolMarkerDir on first detection so the next run can skip the
50+
// LookPath syscall + cache-dir mkdir.
51+
func toolAvailable(name string) bool {
52+
marker := filepath.Join(toolMarkerDir, name)
53+
54+
// Fast path: marker present AND the binary still resolves.
55+
if _, err := os.Stat(marker); err == nil {
56+
if _, err := exec.LookPath(name); err == nil {
57+
return true
58+
}
59+
// Marker stale -- the user uninstalled or moved the tool. Drop
60+
// the marker so a successful re-detect re-creates it.
61+
_ = os.Remove(marker)
62+
63+
return false
64+
}
65+
66+
if _, err := exec.LookPath(name); err != nil {
67+
return false
68+
}
69+
// First-time detection: persist the marker. Best-effort; failures
70+
// just mean the next run repeats the LookPath, which is cheap.
71+
_ = os.MkdirAll(toolMarkerDir, 0o755)
72+
_ = os.WriteFile(marker, nil, 0o644)
73+
74+
return true
75+
}
76+
77+
// printMissingToolsHint writes a tailored install hint to stderr based
78+
// on the detected package manager (or distro family on Linux).
79+
func printMissingToolsHint(missing []string) {
80+
fmt.Fprintf(os.Stderr, "error: tin needs these tools but couldn't find them in $PATH: %s\n\n",
81+
strings.Join(missing, ", "))
82+
83+
switch runtime.GOOS {
84+
case "darwin":
85+
fmt.Fprintln(os.Stderr, "Install with Homebrew:")
86+
fmt.Fprintln(os.Stderr, " brew install llvm")
87+
fmt.Fprintln(os.Stderr)
88+
fmt.Fprintln(os.Stderr, "Homebrew's LLVM is keg-only -- if it's already installed,")
89+
fmt.Fprintln(os.Stderr, "you likely need to add its bin directory to your PATH:")
90+
fmt.Fprintln(os.Stderr, " export PATH=\"$(brew --prefix llvm)/bin:$PATH\"")
91+
92+
case "linux":
93+
switch detectLinuxPM() {
94+
case "apt":
95+
fmt.Fprintln(os.Stderr, "Install on Debian / Ubuntu:")
96+
fmt.Fprintln(os.Stderr, " sudo apt-get install clang lld llvm")
97+
case "pacman":
98+
fmt.Fprintln(os.Stderr, "Install on Arch / Manjaro:")
99+
fmt.Fprintln(os.Stderr, " sudo pacman -S clang lld llvm")
100+
case "dnf":
101+
fmt.Fprintln(os.Stderr, "Install on Fedora / RHEL / CentOS Stream:")
102+
fmt.Fprintln(os.Stderr, " sudo dnf install clang lld llvm")
103+
case "zypper":
104+
fmt.Fprintln(os.Stderr, "Install on openSUSE:")
105+
fmt.Fprintln(os.Stderr, " sudo zypper install clang lld llvm")
106+
case "apk":
107+
fmt.Fprintln(os.Stderr, "Install on Alpine:")
108+
fmt.Fprintln(os.Stderr, " apk add clang lld llvm")
109+
case "xbps":
110+
fmt.Fprintln(os.Stderr, "Install on Void:")
111+
fmt.Fprintln(os.Stderr, " sudo xbps-install -S clang lld llvm")
112+
default:
113+
fmt.Fprintln(os.Stderr, "Install via your distro's package manager. Typical names:")
114+
fmt.Fprintln(os.Stderr, " clang, lld, llvm")
115+
}
116+
117+
fmt.Fprintln(os.Stderr)
118+
fmt.Fprintln(os.Stderr, "If LLVM is already installed but the binaries aren't on PATH,")
119+
fmt.Fprintln(os.Stderr, "look under /usr/lib/llvm-*/bin or /opt/llvm/bin and add that to PATH.")
120+
121+
default:
122+
fmt.Fprintln(os.Stderr, "Install LLVM (clang + opt + lld) via your platform's package manager.")
123+
}
124+
}
125+
126+
// detectLinuxPM returns the canonical name of the most popular package
127+
// manager whose driver binary is on $PATH. Best-effort -- when nothing
128+
// matches we return "" and the caller falls back to a generic message.
129+
func detectLinuxPM() string {
130+
for _, candidate := range []struct{ bin, name string }{
131+
{"apt-get", "apt"},
132+
{"pacman", "pacman"},
133+
{"dnf", "dnf"},
134+
{"zypper", "zypper"},
135+
{"apk", "apk"},
136+
{"xbps-install", "xbps"},
137+
} {
138+
if _, err := exec.LookPath(candidate.bin); err == nil {
139+
return candidate.name
140+
}
141+
}
142+
143+
return ""
144+
}

0 commit comments

Comments
 (0)