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}