use crate::catalog::{Catalog, ResolutionContext, Resolved};
use cuttlefish_abi::{Signature, Ty};
use std::path::{Path, PathBuf};
use wasmtime::Engine;
pub struct Stage {
pub name: String,
pub kind: crate::catalog::ArtifactKind,
pub resolved: Option<String>,
pub module_bytes: Vec<u8>,
pub signature: Signature,
pub script: Option<String>,
}
pub struct ResolvedInput {
pub name: String,
pub kind: crate::catalog::ArtifactKind,
pub resolved: Option<String>,
pub bytes: Vec<u8>,
pub script: Option<String>,
}
impl std::fmt::Debug for ResolvedInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResolvedInput")
.field("name", &self.name)
.field("kind", &self.kind)
.field("resolved", &self.resolved)
.field("bytes", &format!("<{} bytes>", self.bytes.len()))
.field("script", &self.script)
.finish()
}
}
pub struct Checked {
stages: Vec<Stage>,
}
impl Checked {
pub fn stages(&self) -> &[Stage] {
&self.stages
}
pub fn input(&self) -> &Ty {
&self.stages[0].signature.input
}
pub fn output(&self) -> &Ty {
&self.stages[self.stages.len() - 1].signature.output
}
}
#[derive(Debug, thiserror::Error)]
pub enum PipelineError {
#[error("reading block {path}: {source}")]
Unreadable {
path: PathBuf,
source: std::io::Error,
},
#[error("inspecting {name}: {message}")]
Uninspectable {
name: String,
message: String,
},
#[error(transparent)]
Resolution(#[from] crate::catalog::CatalogError),
#[error(
"block {consumer} expects {expected}, but {producer} before it produces {produced}.\n\
Adjust one of the two signatures, or insert a block that converts between them."
)]
SeamMismatch {
producer: String,
produced: String,
consumer: String,
expected: String,
},
#[error("a pipeline needs at least one block")]
Empty,
}
pub fn check(engine: &Engine, inputs: &[ResolvedInput]) -> Result<Checked, PipelineError> {
if inputs.is_empty() {
return Err(PipelineError::Empty);
}
let mut stages: Vec<Stage> = Vec::with_capacity(inputs.len());
for input in inputs {
let signature = read_stage_signature(engine, input)?;
if let Some(previous) = stages.last() {
if !previous.signature.output.assignable_to(&signature.input) {
return Err(PipelineError::SeamMismatch {
producer: previous.name.clone(),
produced: previous.signature.output.to_string(),
consumer: input.name.clone(),
expected: signature.input.to_string(),
});
}
}
stages.push(Stage {
name: input.name.clone(),
kind: input.kind,
resolved: input.resolved.clone(),
module_bytes: input.bytes.clone(),
signature,
script: input.script.clone(),
});
}
Ok(Checked { stages })
}
pub fn read_stage_signature(
engine: &Engine,
input: &ResolvedInput,
) -> Result<Signature, PipelineError> {
match input.kind {
crate::catalog::ArtifactKind::Block => crate::runner::read_signature(engine, &input.bytes)
.map_err(|e| PipelineError::Uninspectable {
name: input.name.clone(),
message: format!("{e:#}"),
}),
crate::catalog::ArtifactKind::Bundle => {
let compact = crate::catalog::read_bundle_signature(&input.bytes, &input.name)
.map_err(|e| PipelineError::Uninspectable {
name: input.name.clone(),
message: match e {
crate::catalog::CatalogError::UninspectableArtifact { reason, .. } => {
reason
}
other => other.to_string(),
},
})?;
compact
.parse::<Signature>()
.map_err(|e| PipelineError::Uninspectable {
name: input.name.clone(),
message: format!("cached signature `{compact}` does not parse: {e}"),
})
}
crate::catalog::ArtifactKind::Script => {
let script = input
.script
.as_deref()
.ok_or_else(|| PipelineError::Uninspectable {
name: input.name.clone(),
message: "a Script-kind ResolvedInput with no script text — this is an \
internal bug in resolve_and_load, not a user-facing error"
.to_string(),
})?;
let compact = crate::catalog::read_script_signature(script.as_bytes(), &input.name)
.map_err(|e| PipelineError::Uninspectable {
name: input.name.clone(),
message: match e {
crate::catalog::CatalogError::UninspectableArtifact { reason, .. } => {
reason
}
other => other.to_string(),
},
})?;
compact
.parse::<Signature>()
.map_err(|e| PipelineError::Uninspectable {
name: input.name.clone(),
message: format!("cached signature `{compact}` does not parse: {e}"),
})
}
}
}
pub fn resolve_and_load(
catalog: &Catalog,
spec_dir: &Path,
entry: &str,
context: ResolutionContext,
) -> Result<ResolvedInput, PipelineError> {
let candidate = spec_dir.join(entry);
let use_joined = candidate.exists() || entry.ends_with(".wasm") || entry.ends_with(".cfbundle");
let joined;
let s: &str = if use_joined {
joined = candidate.to_string_lossy().into_owned();
&joined
} else {
entry
};
match catalog.resolve(s, context)? {
Resolved::Direct(path) => {
if path.is_dir() {
return Err(PipelineError::Uninspectable {
name: path.display().to_string(),
message: "this is a directory. A pipeline names compiled \
`.wasm`/`.cfbundle` artifacts, not block source \
directories — build the block first and point at \
the compiled artifact."
.into(),
});
}
let bytes = std::fs::read(&path).map_err(|source| PipelineError::Unreadable {
path: path.clone(),
source,
})?;
if path.extension().is_some_and(|e| e == "rhai") {
let script =
String::from_utf8(bytes).map_err(|e| PipelineError::Uninspectable {
name: path.display().to_string(),
message: format!("script is not valid UTF-8: {e}"),
})?;
return Ok(ResolvedInput {
name: name_of(&path),
kind: crate::catalog::ArtifactKind::Script,
resolved: None,
bytes: crate::embedded_rhai_interpreter_bytes().to_vec(),
script: Some(script),
});
}
let kind = crate::catalog::sniff_artifact_kind(&bytes).ok_or_else(|| {
let message = if path.extension().is_some_and(|e| e == "rhai") {
"not a recognized artifact: a .rhai script must be `cuttlefish catalog \
add`ed before use — a pipeline can't reference one directly by path"
.to_string()
} else {
"not a recognized artifact (neither wasm nor .cfbundle magic bytes)".into()
};
PipelineError::Uninspectable {
name: path.display().to_string(),
message,
}
})?;
Ok(ResolvedInput {
name: name_of(&path),
kind,
resolved: None,
bytes,
script: None,
})
}
Resolved::Cataloged {
name_version,
entry,
} => {
let name = name_version
.split_once('@')
.map(|(n, _)| n)
.unwrap_or(&name_version)
.to_string();
if entry.kind == crate::catalog::ArtifactKind::Script {
let script_bytes = catalog.read_blob(&entry)?;
let script =
String::from_utf8(script_bytes).map_err(|e| PipelineError::Uninspectable {
name: name.clone(),
message: format!("cataloged script is not valid UTF-8: {e}"),
})?;
return Ok(ResolvedInput {
name,
kind: entry.kind,
resolved: Some(name_version),
bytes: crate::embedded_rhai_interpreter_bytes().to_vec(),
script: Some(script),
});
}
let bytes = catalog.read_blob(&entry)?;
Ok(ResolvedInput {
name,
kind: entry.kind,
resolved: Some(name_version),
bytes,
script: None,
})
}
}
}
fn name_of(path: &Path) -> String {
path.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string())
}