Skip to main content

isoloom_spec/syntax/step/
software.rs

1//! The steps that install software: `packages` (each by name, with its version or without) and
2//! `repository` (a vendor's package repository, its key pinned by SHA-256).
3
4use std::fmt;
5
6use schemars::JsonSchema;
7use serde::de::{self, MapAccess, Visitor};
8use serde::{Deserialize, Deserializer};
9
10use super::super::mistake::Mistake;
11use super::super::problem::Code;
12use super::words::{AptWord, HttpsUrl, PackageName, PackageVersion, RepositoryName, Sha256};
13
14/// A package to install: its name, and its version as its family's package manager writes it
15/// (`1.30.6-1~bookworm`) when it is pinned. On Windows, a Windows feature (`Web-Server`).
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
17#[serde(from = "PackageInput")]
18pub struct Package {
19    /// Its name.
20    pub name: PackageName,
21    /// Its exact version, when pinned.
22    pub version: Option<PackageVersion>,
23}
24
25impl fmt::Display for Package {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match &self.version {
28            Some(v) => write!(f, "{} {v}", self.name),
29            None => write!(f, "{}", self.name),
30        }
31    }
32}
33
34/// A package written in full.
35#[derive(Deserialize, JsonSchema)]
36#[serde(deny_unknown_fields)]
37struct PackageFull {
38    /// The package's name.
39    name: PackageName,
40    /// Its exact version, as the family's package manager writes it (`1.30.6-1~bookworm`,
41    /// `1:2.4.62-1`, `1.30.6-r1`), quoted.
42    #[serde(default)]
43    version: Option<PackageVersion>,
44}
45
46// What the schema shows and the reader returns.
47/// A package as written, for the schema.
48#[derive(JsonSchema)]
49#[serde(untagged)]
50enum PackageInput {
51    /// Its name: the version the package manager picks.
52    Name(PackageName),
53    /// Its name and its exact version.
54    Pinned(PackageFull),
55}
56
57schema_as!(Package, PackageInput);
58
59impl From<PackageInput> for Package {
60    fn from(written: PackageInput) -> Package {
61        match written {
62            PackageInput::Name(name) => Package { name, version: None },
63            PackageInput::Pinned(full) => Package {
64                name: full.name,
65                version: full.version,
66            },
67        }
68    }
69}
70
71impl<'de> Deserialize<'de> for PackageInput {
72    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<PackageInput, D::Error> {
73        d.deserialize_any(PackageVisitor)
74    }
75}
76
77/// A package as written: its name, or `{ name, version }`.
78struct PackageVisitor;
79
80impl<'de> Visitor<'de> for PackageVisitor {
81    type Value = PackageInput;
82
83    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
84        f.write_str("a package's name, or `{ name, version }`")
85    }
86
87    fn visit_str<E: de::Error>(self, v: &str) -> Result<PackageInput, E> {
88        if let Some((name, version)) = v.split_once('=') {
89            return Err(E::custom(
90                Mistake::new(Code::InvalidValue, format!("`{v}`: a package's version goes in `version`"))
91                    .fix(format!("`{{ name: {name}, version: \"{version}\" }}`")),
92            ));
93        }
94        v.parse().map(PackageInput::Name).map_err(E::custom)
95    }
96
97    fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<PackageInput, A::Error> {
98        PackageFull::deserialize(de::value::MapAccessDeserializer::new(map)).map(PackageInput::Pinned)
99    }
100}
101
102/// A `repository` step: a vendor's package repository added to the machine's package manager,
103/// its key pinned by the SHA-256 of its file. Its kind (apt, rpm or apk) is the OS family's.
104#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, JsonSchema)]
105#[serde(deny_unknown_fields)]
106pub struct Repository {
107    /// Its name, in kebab case: it names the source and key files (`nginx`).
108    pub name: RepositoryName,
109    /// Its address (`https`).
110    pub url: HttpsUrl,
111    /// Its signing key: `{ url, sha256 }`, the SHA-256 of the key's file.
112    pub key: RepositoryKey,
113    /// The suite (apt only; default: the OS's codename, `bookworm`).
114    #[serde(default)]
115    pub suite: Option<AptWord>,
116    /// The components (apt only; default `[main]`).
117    #[serde(default)]
118    pub components: Option<Vec<AptWord>>,
119    /// Its priority over the OS's own packages: apt's pin priority, dnf's `priority`.
120    #[serde(default)]
121    pub priority: Option<i32>,
122}
123
124/// A repository's signing key, pinned.
125#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, JsonSchema)]
126#[serde(deny_unknown_fields)]
127pub struct RepositoryKey {
128    /// Where the key is (`https`).
129    pub url: HttpsUrl,
130    /// The SHA-256 of the key's file (64 hex digits, as `sha256sum` prints it).
131    pub sha256: Sha256,
132}