tatara_build_remote/
lib.rs1#![forbid(unsafe_code)]
36
37pub mod transports;
38
39pub use transports::{AtticTransport, LocalTransport, SshRemoteTransport};
40
41use serde::{Deserialize, Serialize};
42use thiserror::Error;
43
44#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
46#[serde(tag = "kind", content = "value", rename_all = "kebab-case")]
47pub enum BuildRef {
48 Flake { url: String, attr: String },
50 Nix { expr: String },
52 StorePath(String),
54 Oci { image: String, tag: String },
56}
57
58#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
60#[serde(rename_all = "camelCase")]
61pub struct BuildTransportChain {
62 pub attic: Option<String>,
64 pub remote: Option<String>,
66 #[serde(default = "yes")]
68 pub local: bool,
69}
70
71fn yes() -> bool {
72 true
73}
74
75impl BuildTransportChain {
76 #[must_use]
78 pub fn quero_lol() -> Self {
79 Self {
80 attic: Some("quero.lol".into()),
81 remote: Some("ssh://builder.quero.lol".into()),
82 local: true,
83 }
84 }
85
86 #[must_use]
90 pub fn to_layered(&self) -> LayeredTransport {
91 let mut transports: Vec<Box<dyn BuildTransport + Send + Sync>> = Vec::new();
92 if let Some(cache) = &self.attic {
93 transports.push(Box::new(AtticTransport::new(cache.clone())));
94 }
95 if let Some(ssh) = &self.remote {
96 transports.push(Box::new(SshRemoteTransport::new(ssh.clone())));
97 }
98 if self.local {
99 transports.push(Box::new(LocalTransport::default()));
100 }
101 LayeredTransport { transports }
102 }
103
104 #[must_use]
106 pub fn local_only() -> Self {
107 Self {
108 attic: None,
109 remote: None,
110 local: true,
111 }
112 }
113
114 #[must_use]
116 pub fn remote_only(ssh: impl Into<String>) -> Self {
117 Self {
118 attic: None,
119 remote: Some(ssh.into()),
120 local: false,
121 }
122 }
123}
124
125pub trait BuildTransport {
128 fn fetch(&self, reference: &BuildRef) -> Result<StorePath, BuildError>;
134}
135
136#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
138pub struct StorePath(pub String);
139
140pub struct LayeredTransport {
142 pub transports: Vec<Box<dyn BuildTransport + Send + Sync>>,
143}
144
145impl BuildTransport for LayeredTransport {
146 fn fetch(&self, r: &BuildRef) -> Result<StorePath, BuildError> {
147 let mut last_err = None;
148 for t in &self.transports {
149 match t.fetch(r) {
150 Ok(p) => return Ok(p),
151 Err(e) => last_err = Some(e),
152 }
153 }
154 Err(last_err.unwrap_or(BuildError::AllTransportsFailed))
155 }
156}
157
158#[derive(Debug, Error)]
159pub enum BuildError {
160 #[error("attic: {0}")]
161 Attic(String),
162 #[error("remote ssh-ng: {0}")]
163 Remote(String),
164 #[error("local nix build: {0}")]
165 Local(String),
166 #[error("all transports failed")]
167 AllTransportsFailed,
168 #[error("transport not configured: {0}")]
169 NotConfigured(String),
170}
171
172pub const CRATE_STATUS: &str = "phase-h5";
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn build_ref_json_round_trip() {
181 let r = BuildRef::Flake {
182 url: "github:pleme-io/tatara-os".into(),
183 attr: "kernel".into(),
184 };
185 let j = serde_json::to_string(&r).unwrap();
186 let back: BuildRef = serde_json::from_str(&j).unwrap();
187 assert_eq!(r, back);
188 }
189
190 #[test]
191 fn quero_lol_preset_is_full_chain() {
192 let c = BuildTransportChain::quero_lol();
193 assert_eq!(c.attic.as_deref(), Some("quero.lol"));
194 assert_eq!(c.remote.as_deref(), Some("ssh://builder.quero.lol"));
195 assert!(c.local);
196 }
197
198 #[test]
199 fn remote_only_refuses_local() {
200 let c = BuildTransportChain::remote_only("ssh://foo.example");
201 assert!(!c.local);
202 assert_eq!(c.remote.as_deref(), Some("ssh://foo.example"));
203 }
204
205 #[test]
206 fn local_only_has_no_remote() {
207 let c = BuildTransportChain::local_only();
208 assert!(c.attic.is_none());
209 assert!(c.remote.is_none());
210 assert!(c.local);
211 }
212
213 #[test]
214 fn quero_lol_to_layered_builds_three_transports() {
215 let chain = BuildTransportChain::quero_lol();
216 let layered = chain.to_layered();
217 assert_eq!(layered.transports.len(), 3);
218 }
219
220 #[test]
221 fn remote_only_to_layered_has_one_transport() {
222 let chain = BuildTransportChain::remote_only("ssh://foo.example");
223 let layered = chain.to_layered();
224 assert_eq!(layered.transports.len(), 1);
225 }
226
227 #[test]
228 fn local_only_to_layered_has_one_transport() {
229 let chain = BuildTransportChain::local_only();
230 let layered = chain.to_layered();
231 assert_eq!(layered.transports.len(), 1);
232 }
233
234 #[test]
235 fn empty_chain_to_layered_has_no_transports() {
236 let chain = BuildTransportChain {
237 attic: None,
238 remote: None,
239 local: false,
240 };
241 let layered = chain.to_layered();
242 assert!(layered.transports.is_empty());
243 }
244}