Skip to main content

riff_core/
build.rs

1//! The build of `riff` and `riff-server`, and the check that they can
2//! talk.
3//!
4//! # Design
5//!
6//! A build is the crate version, the last commit that changed the code
7//! (`crates`, `Cargo.toml` or `Cargo.lock`), and the UTC time of that
8//! commit. `build.rs` finds the commit. The image build of
9//! `riff-server` has no repository: it gives `RIFF_COMMIT` and
10//! `RIFF_COMMIT_TIME`, from `deploy/build-id.sh`, which runs the same
11//! command.
12//!
13//! The crate version is a semantic version, and it says what a build
14//! can talk to (01M3MX1DYY6AVDW946NR0B9T2C). The line of a version is
15//! its major, and its minor while the major is 0: `0.4.1` is on the
16//! line `0.4`, `1.2.0` on the line `1`. A change that another machine
17//! or session can notice, for example to a wire type, to the API or to
18//! the header, starts a new line (01M3N73EAAD1G88TG7SNFWE4P1).
19//! Most merges change no wire type, so the builds differ and the line
20//! matches: `riff` goes on, and tells the person once to update
21//! ([`other_build`]).
22//!
23//! `riff-server` also takes a `riff` of the line before its own
24//! (01M3MX1E1EY1M7JGNCN6FCEVQK), so a person has one release to update
25//! in. It takes no `riff` of another line. [`compatible`] holds the
26//! rule for both sides. It reads the version of each side at run time:
27//! no release number of riff is a constant in the code
28//! (01M5RG6BXBQVFABA1N4NRZBMQF).
29//!
30//! Each call of `riff` carries its build in the header [`HEADER`], and
31//! each reply of `riff-server` carries the build of the server
32//! (01M3JEE7P46GWXR1BD4Q1TTSGN). Each side refuses a version that it
33//! cannot talk to, or no build (01M3MX1E65XGWDZ062PQ9YXQ5T):
34//!
35//! ```mermaid
36//! sequenceDiagram
37//!     participant R as riff
38//!     participant S as riff-server
39//!     R->>S: call, riff-build: 0.3.2 929605821e54 2026-09-27T22:03:01Z
40//!     alt the line of the server, or the line before
41//!         S-->>R: reply, riff-build: 0.4.0 7213825ab1c2 2026-09-28T20:10:44Z
42//!         Note over R: another build: one note to update, then go on
43//!     else another line, or no build
44//!         S-->>R: 409, riff-build: the build of the server, the mismatch
45//!     end
46//!     Note over R: a reply with no build, or of a line that riff<br/>cannot talk to, is a Mismatch too
47//! ```
48
49use std::fmt;
50
51/// The HTTP header that carries a [`Build`].
52pub const HEADER: &str = "riff-build";
53
54/// The build of this binary as text, for `--version` and [`HEADER`]
55/// (01M3JEE7WT04BKX377VW5GDSPY).
56///
57/// ```
58/// use riff_core::build::{Build, VERSION};
59///
60/// assert_eq!(VERSION, Build::this().to_string());
61/// assert!(VERSION.starts_with(env!("CARGO_PKG_VERSION")));
62/// ```
63pub const VERSION: &str = concat!(
64    env!("CARGO_PKG_VERSION"),
65    " ",
66    env!("RIFF_BUILD_COMMIT"),
67    " ",
68    env!("RIFF_BUILD_TIME"),
69);
70
71/// The part of the book that tells how to update each side.
72pub const UPDATE_URL: &str =
73    "https://como-technologies.github.io/riff/how-it-works.html#when-the-versions-do-not-match";
74
75/// A semantic version: major, minor and patch. A pre-release or build
76/// part after `-` or `+` is not part of the order.
77///
78/// ```
79/// use riff_core::build::Semver;
80///
81/// let v: Semver = "0.4.1".parse().unwrap();
82/// assert_eq!(v, Semver { major: 0, minor: 4, patch: 1 });
83/// assert_eq!(v.line(), "0.4");
84/// assert_eq!("1.2.0-rc1".parse::<Semver>().unwrap().line(), "1");
85/// assert!("0.4".parse::<Semver>().is_err());
86/// assert!("v0.4.1".parse::<Semver>().is_err());
87/// ```
88#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
89pub struct Semver {
90    pub major: u64,
91    pub minor: u64,
92    pub patch: u64,
93}
94
95impl Semver {
96    /// The line of the version: the major, and the minor while the
97    /// major is 0. Builds of one line talk.
98    fn key(self) -> (u64, u64) {
99        if self.major == 0 {
100            (0, self.minor)
101        } else {
102            (self.major, 0)
103        }
104    }
105
106    /// The line as text: `0.4` or `1`.
107    pub fn line(self) -> String {
108        match self.key() {
109            (0, minor) => format!("0.{minor}"),
110            (major, _) => major.to_string(),
111        }
112    }
113
114    /// The first version of the line before this one. `None` for the
115    /// line `0.0`, and for the first major: the last minor line of the
116    /// major 0 is not known.
117    ///
118    /// ```
119    /// use riff_core::build::Semver;
120    ///
121    /// let v = |s: &str| s.parse::<Semver>().unwrap();
122    /// assert_eq!(v("0.4.2").line_before(), Some(v("0.3.0")));
123    /// assert_eq!(v("2.1.0").line_before(), Some(v("1.0.0")));
124    /// assert_eq!(v("1.0.0").line_before(), None);
125    /// assert_eq!(v("0.0.9").line_before(), None);
126    /// ```
127    pub fn line_before(self) -> Option<Semver> {
128        match self.key() {
129            (0, 0) | (1, _) => None,
130            (0, minor) => Some(Semver {
131                major: 0,
132                minor: minor - 1,
133                patch: 0,
134            }),
135            (major, _) => Some(Semver {
136                major: major - 1,
137                minor: 0,
138                patch: 0,
139            }),
140        }
141    }
142
143    /// The first version of the line after this one.
144    ///
145    /// ```
146    /// use riff_core::build::Semver;
147    ///
148    /// let v = |s: &str| s.parse::<Semver>().unwrap();
149    /// assert_eq!(v("0.4.2").line_after(), v("0.5.0"));
150    /// assert_eq!(v("1.3.2").line_after(), v("2.0.0"));
151    /// ```
152    pub fn line_after(self) -> Semver {
153        match self.key() {
154            (0, minor) => Semver {
155                major: 0,
156                minor: minor + 1,
157                patch: 0,
158            },
159            (major, _) => Semver {
160                major: major + 1,
161                minor: 0,
162                patch: 0,
163            },
164        }
165    }
166
167    /// True when the two versions are on the same line.
168    ///
169    /// ```
170    /// use riff_core::build::Semver;
171    ///
172    /// let v = |s: &str| s.parse::<Semver>().unwrap();
173    /// assert!(v("0.4.0").same_line(v("0.4.3")));
174    /// assert!(!v("0.4.0").same_line(v("0.5.0")));
175    /// assert!(v("1.2.0").same_line(v("1.9.4")));
176    /// ```
177    pub fn same_line(self, other: Semver) -> bool {
178        self.key() == other.key()
179    }
180
181    /// The lines of `riff` that a `riff-server` of this version talks
182    /// with, as the first version of each line: its own line
183    /// (01M3MX1DYY6AVDW946NR0B9T2C) and the line before
184    /// (01M3MX1E1EY1M7JGNCN6FCEVQK).
185    ///
186    /// ```
187    /// use riff_core::build::Semver;
188    ///
189    /// let v = |s: &str| s.parse::<Semver>().unwrap();
190    /// assert_eq!(v("0.4.2").riff_lines(), [v("0.4.0"), v("0.3.0")]);
191    /// assert_eq!(v("1.3.0").riff_lines(), [v("1.0.0")]);
192    /// assert_eq!(v("4.1.0").riff_lines(), [v("4.0.0"), v("3.0.0")]);
193    /// ```
194    pub fn riff_lines(self) -> Vec<Semver> {
195        [Some(self.first()), self.line_before()]
196            .into_iter()
197            .flatten()
198            .collect()
199    }
200
201    /// The first version of the line of this version.
202    fn first(self) -> Semver {
203        match self.key() {
204            (0, minor) => Semver {
205                major: 0,
206                minor,
207                patch: 0,
208            },
209            (major, _) => Semver {
210                major,
211                minor: 0,
212                patch: 0,
213            },
214        }
215    }
216}
217
218impl fmt::Display for Semver {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
221    }
222}
223
224impl std::str::FromStr for Semver {
225    type Err = String;
226
227    fn from_str(s: &str) -> Result<Self, String> {
228        let core = s.split(['-', '+']).next().unwrap_or_default();
229        let parts: Vec<u64> = core
230            .split('.')
231            .map(str::parse)
232            .collect::<Result<_, _>>()
233            .map_err(|_| format!("{s:?} is not a semantic version"))?;
234        match parts[..] {
235            [major, minor, patch] => Ok(Semver {
236                major,
237                minor,
238                patch,
239            }),
240            _ => Err(format!("{s:?} is not a semantic version")),
241        }
242    }
243}
244
245/// One build of riff: the crate version, the commit and its UTC time.
246///
247/// ```
248/// use riff_core::build::Build;
249///
250/// let b: Build = "0.4.1 929605821e54 2026-09-27T22:03:01Z".parse().unwrap();
251/// assert_eq!(b.commit, "929605821e54");
252/// assert_eq!(b.semver().unwrap().minor, 4);
253/// assert_eq!(b.to_string(), "0.4.1 929605821e54 2026-09-27T22:03:01Z");
254/// // A build of an older form, or with no semantic version, is not a build.
255/// assert!("0.2.0 929605821e54 2026-09-27T22:03:01Z wire 1".parse::<Build>().is_err());
256/// assert!("next 929605821e54 2026-09-27T22:03:01Z".parse::<Build>().is_err());
257/// ```
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct Build {
260    /// The crate version.
261    pub version: String,
262    /// The last commit that changed the code.
263    pub commit: String,
264    /// The time of that commit, in UTC.
265    pub time: String,
266}
267
268impl Build {
269    /// The build of this binary.
270    ///
271    /// ```
272    /// let me = riff_core::build::Build::this();
273    /// assert_eq!(me.version, env!("CARGO_PKG_VERSION"));
274    /// assert!(!me.commit.is_empty());
275    /// ```
276    pub fn this() -> Build {
277        Build {
278            version: env!("CARGO_PKG_VERSION").into(),
279            commit: env!("RIFF_BUILD_COMMIT").into(),
280            time: env!("RIFF_BUILD_TIME").into(),
281        }
282    }
283
284    /// The build that a reply or a request names in [`HEADER`]. `None`
285    /// when it names none, or names it in another form.
286    pub fn from_header(value: Option<&[u8]>) -> Option<Build> {
287        std::str::from_utf8(value?).ok()?.parse().ok()
288    }
289
290    /// The semantic version of the build. `None` when the crate version
291    /// is not one.
292    pub fn semver(&self) -> Option<Semver> {
293        self.version.parse().ok()
294    }
295
296    /// True when the two are the same build: the same version and
297    /// commit.
298    ///
299    /// ```
300    /// use riff_core::build::Build;
301    ///
302    /// let a: Build = "0.4.0 aaaa 2026-09-27T10:00:00Z".parse().unwrap();
303    /// let b: Build = "0.4.0 bbbb 2026-09-27T11:00:00Z".parse().unwrap();
304    /// assert!(a.matches(&a.clone()));
305    /// assert!(!a.matches(&b));
306    /// ```
307    pub fn matches(&self, other: &Build) -> bool {
308        self.version == other.version && self.commit == other.commit
309    }
310
311    /// True when this build is older than `other`: a lower version, or
312    /// the same version from an older commit (01M514C19H3WQP1FSX9RBVSC69).
313    /// The times are UTC in one form, so their text has the order of
314    /// time. With no semantic version on a side, it is false.
315    ///
316    /// ```
317    /// use riff_core::build::Build;
318    ///
319    /// let b = |s: &str| s.parse::<Build>().unwrap();
320    /// let server = b("1.4.0 122daab96637 2026-10-15T23:08:34Z");
321    /// assert!(b("1.3.1 aaaa 2026-10-14T10:00:00Z").older_than(&server));
322    /// assert!(b("1.4.0 bbbb 2026-10-15T20:00:00Z").older_than(&server));
323    /// assert!(!server.older_than(&server));
324    /// assert!(!b("1.4.0 cccc 2026-10-16T01:00:00Z").older_than(&server));
325    /// // A higher version from an older commit is not older.
326    /// assert!(!b("1.5.0 dddd 2026-10-01T00:00:00Z").older_than(&server));
327    /// ```
328    pub fn older_than(&self, other: &Build) -> bool {
329        let (Some(mine), Some(theirs)) = (self.semver(), other.semver()) else {
330            return false;
331        };
332        (mine, &self.time) < (theirs, &other.time)
333    }
334}
335
336/// True when `riff` and `riff-server` can talk: `riff` is on a line
337/// that the server takes ([`Semver::riff_lines`]). That is the line of
338/// the server (01M3MX1DYY6AVDW946NR0B9T2C), or the line before
339/// (01M3MX1E1EY1M7JGNCN6FCEVQK). Each side checks the other with it:
340/// `riff` each reply, and `riff-server` each call.
341///
342/// ```
343/// use riff_core::build::{Build, compatible};
344///
345/// let b = |v: &str| Build { version: v.into(), ..Build::this() };
346/// assert!(compatible(&b("0.4.0"), &b("0.4.3")));
347/// assert!(compatible(&b("0.4.3"), &b("0.4.0")));
348/// assert!(compatible(&b("0.3.2"), &b("0.4.0")));
349/// assert!(!compatible(&b("0.4.0"), &b("0.3.2")));
350/// assert!(!compatible(&b("0.2.0"), &b("0.4.0")));
351/// assert!(compatible(&b("1.0.0"), &b("1.3.0")));
352/// assert!(!compatible(&b("next"), &b("0.4.0")));
353///
354/// // The same rule with the version of this build: the line before
355/// // talks, and two lines before do not.
356/// let this = Build::this();
357/// let line = this.semver().unwrap();
358/// let before = line.line_before().unwrap();
359/// assert!(compatible(&this, &this));
360/// assert!(compatible(&b(&before.to_string()), &this));
361/// assert!(!compatible(&b(&before.line_before().unwrap().to_string()), &this));
362/// assert!(!compatible(&b(&line.line_after().to_string()), &this));
363/// ```
364pub fn compatible(riff: &Build, server: &Build) -> bool {
365    let (Some(r), Some(s)) = (riff.semver(), server.semver()) else {
366        return false;
367    };
368    s.riff_lines().into_iter().any(|line| r.same_line(line))
369}
370
371impl fmt::Display for Build {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        write!(f, "{} {} {}", self.version, self.commit, self.time)
374    }
375}
376
377impl std::str::FromStr for Build {
378    type Err = String;
379
380    fn from_str(s: &str) -> Result<Self, String> {
381        let parts: Vec<&str> = s.split_whitespace().collect();
382        match parts[..] {
383            [version, commit, time] => {
384                version.parse::<Semver>()?;
385                Ok(Build {
386                    version: version.into(),
387                    commit: commit.into(),
388                    time: time.into(),
389                })
390            }
391            _ => Err(format!("{s:?} is not a riff build")),
392        }
393    }
394}
395
396/// `riff` and its `riff-server` cannot talk
397/// (01M3MX1E65XGWDZ062PQ9YXQ5T). `None` is a side that sent no build,
398/// or a build of an older form.
399#[derive(Debug, Clone, PartialEq, Eq)]
400pub struct Mismatch {
401    /// The build of `riff`.
402    pub riff: Option<Build>,
403    /// The build of `riff-server`.
404    pub server: Option<Build>,
405    /// What `riff` saw of a reply that named no build: its status and
406    /// URL, for example `status 200 OK from http://127.0.0.1:7878/v1/who`
407    /// (01M3QCMJ9F1GRTRRSB4AW9TC3D). `None` on the side of the server.
408    pub seen: Option<String>,
409}
410
411impl Mismatch {
412    /// The side to update: the older version. A side with no build is
413    /// older.
414    ///
415    /// ```
416    /// use riff_core::build::{Build, Mismatch, Side};
417    ///
418    /// let b = |v: &str| Some(Build { version: v.into(), ..Build::this() });
419    /// let m = |riff, server| Mismatch { riff, server, seen: None }.older();
420    /// assert_eq!(m(b("0.2.0"), b("0.4.0")), Side::Riff);
421    /// assert_eq!(m(b("0.4.0"), b("0.3.2")), Side::Server);
422    /// assert_eq!(m(b("0.4.0"), None), Side::Server);
423    /// assert_eq!(m(None, b("0.4.0")), Side::Riff);
424    /// ```
425    pub fn older(&self) -> Side {
426        let version = |b: &Option<Build>| b.as_ref().and_then(Build::semver);
427        match (version(&self.riff), version(&self.server)) {
428            (Some(r), Some(s)) if r > s => Side::Server,
429            (Some(_), None) => Side::Server,
430            _ => Side::Riff,
431        }
432    }
433}
434
435/// The side of a [`Mismatch`] to update.
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub enum Side {
438    /// Update `riff` on the machine of the session.
439    Riff,
440    /// Update the `riff-server` of the riff.
441    Server,
442}
443
444impl fmt::Display for Mismatch {
445    /// ```
446    /// use riff_core::build::{Build, Mismatch, UPDATE_URL};
447    ///
448    /// // A riff two lines before this server.
449    /// let server = Build::this();
450    /// let line = server.semver().unwrap();
451    /// let before = line.line_before().unwrap();
452    /// let two = Build {
453    ///     version: before.line_before().unwrap().to_string(),
454    ///     ..Build::this()
455    /// };
456    /// let m = Mismatch { riff: Some(two.clone()), server: Some(server.clone()), seen: None };
457    /// assert_eq!(
458    ///     m.to_string(),
459    ///     format!(
460    ///         "this riff ({two}) and its riff-server ({server}) do not match. riff-server {} \
461    ///          talks only with riff {} and {}. Update riff on this machine, then start your \
462    ///          sessions again. See {UPDATE_URL}",
463    ///         line.line(),
464    ///         line.line(),
465    ///         before.line()
466    ///     )
467    /// );
468    /// let old = Mismatch { riff: m.riff.clone(), server: None, seen: None };
469    /// assert!(old.to_string().contains("riff-server (an older build)"), "{old}");
470    /// assert!(old.to_string().contains("Update riff-server"), "{old}");
471    /// // riff names what it saw of a reply with no build.
472    /// let seen = Some("status 200 OK from http://127.0.0.1:7878/v1/who".to_string());
473    /// let old = Mismatch { seen, ..old };
474    /// assert!(
475    ///     old.to_string().contains(
476    ///         "its riff-server (no riff build in the reply: status 200 OK from \
477    ///          http://127.0.0.1:7878/v1/who) do not match"
478    ///     ),
479    ///     "{old}"
480    /// );
481    /// ```
482    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483        let name = |b: &Option<Build>| match b {
484            Some(b) => b.to_string(),
485            None => "an older build".into(),
486        };
487        let server = match (&self.server, &self.seen) {
488            (None, Some(seen)) => format!("no riff build in the reply: {seen}"),
489            (server, _) => name(server),
490        };
491        write!(
492            f,
493            "this riff ({}) and its riff-server ({server}) do not match. ",
494            name(&self.riff),
495        )?;
496        if let Some(s) = self.server.as_ref().and_then(Build::semver) {
497            let lines: Vec<String> = s.riff_lines().into_iter().map(Semver::line).collect();
498            write!(
499                f,
500                "riff-server {} talks only with riff {}. ",
501                s.line(),
502                and_list(&lines)
503            )?;
504        }
505        let step = match self.older() {
506            Side::Riff => "Update riff on this machine, then start your sessions again.",
507            Side::Server => "Update riff-server, on the machine of the riff.",
508        };
509        write!(f, "{step} See {UPDATE_URL}")
510    }
511}
512
513impl std::error::Error for Mismatch {}
514
515/// The items as text: `a`, `a and b`, or `a, b and c`.
516fn and_list(items: &[String]) -> String {
517    match items {
518        [] => String::new(),
519        [one] => one.clone(),
520        [rest @ .., last] => format!("{} and {last}", rest.join(", ")),
521    }
522}
523
524/// The note when the builds differ and they can talk
525/// (01M3MX1E8M9TKBN90P4DYKH3H8). `riff` prints it once for each process,
526/// and goes on. When `riff` is on the line before the server, the note
527/// says that the next line of the server refuses it. On the same line,
528/// the note names the older side ([`Build::older_than`]), as `riff
529/// server` does (01M56Q55SHHJSKJWNHMW79X7HA).
530///
531/// ```
532/// use riff_core::build::{Build, Semver, other_build};
533///
534/// let this = Build::this().semver().unwrap();
535/// let at = |version: Semver, commit: &str, time: &str| Build {
536///     version: version.to_string(),
537///     commit: commit.into(),
538///     time: time.into(),
539/// };
540/// // The same line, and the server is newer.
541/// let server = at(Semver { patch: this.patch + 3, ..this }, "aaaa", "2026-09-27T10:00:00Z");
542/// let riff = at(this, "bbbb", "2026-09-27T11:00:00Z");
543/// assert_eq!(
544///     other_build(&riff, &server),
545///     format!(
546///         "riff-server runs build {server}; this riff runs build {riff}. Run riff update when \
547///          you can."
548///     )
549/// );
550/// // The same line, and this riff is newer.
551/// assert_eq!(
552///     other_build(&server, &riff),
553///     format!(
554///         "riff-server runs build {riff}; this riff runs build {server}. Update riff-server \
555///          when you can."
556///     )
557/// );
558/// // The line before the server.
559/// let before = at(this.line_before().unwrap(), "cccc", "2026-09-20T11:00:00Z");
560/// assert_eq!(
561///     other_build(&before, &server),
562///     format!(
563///         "riff-server runs build {server}; this riff runs build {before}. riff-server {} will \
564///          refuse riff {}. Run riff update soon.",
565///         this.line_after().line(),
566///         this.line_before().unwrap().line()
567///     )
568/// );
569/// ```
570pub fn other_build(riff: &Build, server: &Build) -> String {
571    let head = format!("riff-server runs build {server}; this riff runs build {riff}.");
572    match (riff.semver(), server.semver()) {
573        (Some(r), Some(s)) if r.same_line(s) && server.older_than(riff) => {
574            format!("{head} Update riff-server when you can.")
575        }
576        (Some(r), Some(s)) if !r.same_line(s) => format!(
577            "{head} riff-server {} will refuse riff {}. Run riff update soon.",
578            s.line_after().line(),
579            r.line()
580        ),
581        _ => format!("{head} Run riff update when you can."),
582    }
583}