Skip to main content

bynk/
compiler.rs

1//! Locate the `bynkc` compiler the driver shells, and report
2//! **driver↔compiler version skew**.
3//!
4//! Resolution order (ADR: introduce the `bynk` driver):
5//!
6//! 1. an explicit override — the `BYNK_BYNKC` environment variable (the
7//!    `bynk.executablePath`-style escape hatch);
8//! 2. `bynkc` on `PATH`;
9//! 3. a `bynkc` sibling of the running `bynk` binary (mirrors how `vscode-bynk`
10//!    resolves `bynkc-lsp` next to itself).
11//!
12//! An explicit override wins when set — an override that only applied after
13//! auto-discovery failed would be useless. The skew check exists *because* this
14//! resolution can pick a `bynkc` whose version differs from the driver's: once
15//! they are separate binaries, a global `bynk 0.46` can shell a stale `bynkc
16//! 0.44`, and `doctor`'s whole job is to surface exactly that.
17
18use std::path::{Path, PathBuf};
19
20use crate::probe::{Toolbox, Version};
21
22/// How `bynkc` was located.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Origin {
25    /// From the `BYNK_BYNKC` override.
26    Override,
27    /// From the global `PATH`.
28    Path,
29    /// A sibling of the running `bynk` binary.
30    Sibling,
31}
32
33impl Origin {
34    pub fn token(self) -> &'static str {
35        match self {
36            Origin::Override => "override",
37            Origin::Path => "path",
38            Origin::Sibling => "sibling",
39        }
40    }
41}
42
43/// Driver↔compiler version relationship. Patch differences are ignored (they
44/// are wire-compatible under the project's unified versioning); a minor drift
45/// warns; a major drift is a contract mismatch and an error.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Skew {
48    /// Versions match (ignoring patch), or the compiler version is unknown.
49    Match,
50    /// Minor drift — warn (fails only under `--strict`).
51    Minor,
52    /// Major drift — a contract mismatch; an error even on a bare run.
53    Major,
54}
55
56impl Skew {
57    /// Classify the driver version against a resolved compiler version.
58    pub fn classify(driver: Version, compiler: Version) -> Skew {
59        if driver.major != compiler.major {
60            Skew::Major
61        } else if driver.minor != compiler.minor {
62            Skew::Minor
63        } else {
64            Skew::Match
65        }
66    }
67
68    pub fn token(self) -> &'static str {
69        match self {
70            Skew::Match => "match",
71            Skew::Minor => "minor",
72            Skew::Major => "major",
73        }
74    }
75}
76
77/// A resolved (or unresolved) `bynkc`.
78#[derive(Debug, Clone)]
79pub struct Compiler {
80    /// `None` when `bynkc` could not be located at all — the broken compile
81    /// floor, which fails `doctor` even on a bare run.
82    pub path: Option<PathBuf>,
83    pub origin: Option<Origin>,
84    pub version: Option<Version>,
85    /// `None` when there is no compiler, or its version could not be read.
86    pub skew: Option<Skew>,
87}
88
89impl Compiler {
90    pub fn is_resolved(&self) -> bool {
91        self.path.is_some()
92    }
93
94    /// A major skew is a hard floor break even on a bare run.
95    pub fn has_major_skew(&self) -> bool {
96        self.skew == Some(Skew::Major)
97    }
98}
99
100/// Resolve `bynkc` against a [`Toolbox`], given the override (typically
101/// `std::env::var("BYNK_BYNKC")`), the directory of the running `bynk` binary
102/// (for the sibling fallback), and the driver's own version (to classify skew).
103pub fn resolve(
104    tb: &dyn Toolbox,
105    override_path: Option<&Path>,
106    bynk_bin_dir: Option<&Path>,
107    driver: Version,
108) -> Compiler {
109    let (path, origin) = locate(tb, override_path, bynk_bin_dir);
110    let version = path.as_deref().and_then(|p| tb.version(p));
111    let skew = version.map(|v| Skew::classify(driver, v));
112    Compiler {
113        path,
114        origin,
115        version,
116        skew,
117    }
118}
119
120fn locate(
121    tb: &dyn Toolbox,
122    override_path: Option<&Path>,
123    bynk_bin_dir: Option<&Path>,
124) -> (Option<PathBuf>, Option<Origin>) {
125    if let Some(ovr) = override_path {
126        // An explicit override is taken as-is when it resolves; we do not fall
127        // through on a bad override, so a typo surfaces rather than silently
128        // picking a different compiler.
129        if let Some(p) = tb.in_dir(ovr.parent().unwrap_or(Path::new(".")), file_stem(ovr)) {
130            return (Some(p), Some(Origin::Override));
131        }
132        return (Some(ovr.to_path_buf()), Some(Origin::Override));
133    }
134    if let Some(p) = tb.on_path("bynkc") {
135        return (Some(p), Some(Origin::Path));
136    }
137    if let Some(dir) = bynk_bin_dir
138        && let Some(p) = tb.in_dir(dir, "bynkc")
139    {
140        return (Some(p), Some(Origin::Sibling));
141    }
142    (None, None)
143}
144
145fn file_stem(p: &Path) -> &str {
146    p.file_stem().and_then(|s| s.to_str()).unwrap_or("bynkc")
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn skew_classification() {
155        let v = |a, b, c| Version {
156            major: a,
157            minor: b,
158            patch: c,
159        };
160        assert_eq!(Skew::classify(v(0, 46, 0), v(0, 46, 0)), Skew::Match);
161        // patch drift is wire-compatible
162        assert_eq!(Skew::classify(v(0, 46, 0), v(0, 46, 3)), Skew::Match);
163        assert_eq!(Skew::classify(v(0, 46, 0), v(0, 44, 0)), Skew::Minor);
164        assert_eq!(Skew::classify(v(1, 0, 0), v(0, 46, 0)), Skew::Major);
165    }
166}