#![forbid(unsafe_code)]
pub mod transports;
pub use transports::{AtticTransport, LocalTransport, SshRemoteTransport};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(tag = "kind", content = "value", rename_all = "kebab-case")]
pub enum BuildRef {
Flake { url: String, attr: String },
Nix { expr: String },
StorePath(String),
Oci { image: String, tag: String },
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct BuildTransportChain {
pub attic: Option<String>,
pub remote: Option<String>,
#[serde(default = "yes")]
pub local: bool,
}
fn yes() -> bool {
true
}
impl BuildTransportChain {
#[must_use]
pub fn quero_lol() -> Self {
Self {
attic: Some("quero.lol".into()),
remote: Some("ssh://builder.quero.lol".into()),
local: true,
}
}
#[must_use]
pub fn to_layered(&self) -> LayeredTransport {
let mut transports: Vec<Box<dyn BuildTransport + Send + Sync>> = Vec::new();
if let Some(cache) = &self.attic {
transports.push(Box::new(AtticTransport::new(cache.clone())));
}
if let Some(ssh) = &self.remote {
transports.push(Box::new(SshRemoteTransport::new(ssh.clone())));
}
if self.local {
transports.push(Box::new(LocalTransport::default()));
}
LayeredTransport { transports }
}
#[must_use]
pub fn local_only() -> Self {
Self {
attic: None,
remote: None,
local: true,
}
}
#[must_use]
pub fn remote_only(ssh: impl Into<String>) -> Self {
Self {
attic: None,
remote: Some(ssh.into()),
local: false,
}
}
}
pub trait BuildTransport {
fn fetch(&self, reference: &BuildRef) -> Result<StorePath, BuildError>;
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct StorePath(pub String);
pub struct LayeredTransport {
pub transports: Vec<Box<dyn BuildTransport + Send + Sync>>,
}
impl BuildTransport for LayeredTransport {
fn fetch(&self, r: &BuildRef) -> Result<StorePath, BuildError> {
let mut last_err = None;
for t in &self.transports {
match t.fetch(r) {
Ok(p) => return Ok(p),
Err(e) => last_err = Some(e),
}
}
Err(last_err.unwrap_or(BuildError::AllTransportsFailed))
}
}
#[derive(Debug, Error)]
pub enum BuildError {
#[error("attic: {0}")]
Attic(String),
#[error("remote ssh-ng: {0}")]
Remote(String),
#[error("local nix build: {0}")]
Local(String),
#[error("all transports failed")]
AllTransportsFailed,
#[error("transport not configured: {0}")]
NotConfigured(String),
}
pub const CRATE_STATUS: &str = "phase-h5";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_ref_json_round_trip() {
let r = BuildRef::Flake {
url: "github:pleme-io/tatara-os".into(),
attr: "kernel".into(),
};
let j = serde_json::to_string(&r).unwrap();
let back: BuildRef = serde_json::from_str(&j).unwrap();
assert_eq!(r, back);
}
#[test]
fn quero_lol_preset_is_full_chain() {
let c = BuildTransportChain::quero_lol();
assert_eq!(c.attic.as_deref(), Some("quero.lol"));
assert_eq!(c.remote.as_deref(), Some("ssh://builder.quero.lol"));
assert!(c.local);
}
#[test]
fn remote_only_refuses_local() {
let c = BuildTransportChain::remote_only("ssh://foo.example");
assert!(!c.local);
assert_eq!(c.remote.as_deref(), Some("ssh://foo.example"));
}
#[test]
fn local_only_has_no_remote() {
let c = BuildTransportChain::local_only();
assert!(c.attic.is_none());
assert!(c.remote.is_none());
assert!(c.local);
}
#[test]
fn quero_lol_to_layered_builds_three_transports() {
let chain = BuildTransportChain::quero_lol();
let layered = chain.to_layered();
assert_eq!(layered.transports.len(), 3);
}
#[test]
fn remote_only_to_layered_has_one_transport() {
let chain = BuildTransportChain::remote_only("ssh://foo.example");
let layered = chain.to_layered();
assert_eq!(layered.transports.len(), 1);
}
#[test]
fn local_only_to_layered_has_one_transport() {
let chain = BuildTransportChain::local_only();
let layered = chain.to_layered();
assert_eq!(layered.transports.len(), 1);
}
#[test]
fn empty_chain_to_layered_has_no_transports() {
let chain = BuildTransportChain {
attic: None,
remote: None,
local: false,
};
let layered = chain.to_layered();
assert!(layered.transports.is_empty());
}
}