|
| 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