use crate::{CuaError, Result};
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct ResolvedImage {
pub reference: String,
pub variant_ref: String,
pub pinned_ref: String,
pub digest: String,
pub variant: String,
pub arch: Option<String>,
pub architectures: Vec<String>,
pub emulated: bool,
pub os: String,
pub spacesd: Option<bool>,
}
impl From<cua_image::ResolvedImage> for ResolvedImage {
fn from(r: cua_image::ResolvedImage) -> Self {
Self {
reference: r.reference,
variant_ref: r.variant_ref,
pinned_ref: r.pinned_ref,
digest: r.digest,
variant: r.variant.as_str().into(),
arch: r.arch,
architectures: r.architectures,
emulated: r.emulated,
os: r.os,
spacesd: r.spacesd,
}
}
}
fn image_error(e: cua_image::ImageError) -> CuaError {
use cua_image::ImageError as E;
match e {
E::NotFound(m) => CuaError::NotFound(m),
E::Unauthorized(m) => CuaError::Unauthenticated(m),
E::UnsupportedVariant { .. } => CuaError::Unsupported(e.to_string()),
E::Reference(..) => CuaError::InvalidArgument(e.to_string()),
E::NotPublished { .. } => CuaError::ImageNotPublished(e.to_string()),
other => CuaError::Runtime(other.to_string()),
}
}
#[uniffi::export]
pub fn resolve_image(
reference: String,
backend: String,
arch: Option<String>,
) -> Result<ResolvedImage> {
let backend = cua_image::Backend::parse(&backend).map_err(image_error)?;
let arch = arch.unwrap_or_else(|| host_arch().into());
let resolved = std::thread::spawn(move || {
super::runtime().block_on(cua_image::resolve::resolve(&reference, backend, &arch))
})
.join()
.map_err(|_| CuaError::Internal("resolve_image panicked".into()))?
.map_err(image_error)?;
Ok(resolved.into())
}
#[uniffi::export]
pub fn resolve_image_with_secret(
reference: String,
backend: String,
arch: Option<String>,
secret: super::RegistrySecret,
) -> Result<ResolvedImage> {
let backend = cua_image::Backend::parse(&backend).map_err(image_error)?;
let arch = arch.unwrap_or_else(|| host_arch().into());
let resolved = std::thread::spawn(move || {
super::runtime().block_on(async {
let creds = secret.credentials(&reference).await?;
cua_image::resolve::resolve_with_credentials(&reference, backend, &arch, Some(&creds))
.await
.map_err(image_error)
})
})
.join()
.map_err(|_| CuaError::Internal("resolve_image panicked".into()))??;
Ok(resolved.into())
}
#[uniffi::export]
pub fn normalize_image(reference: String) -> Result<String> {
cua_image::resolve::normalize(&reference).map_err(image_error)
}
#[uniffi::export]
pub fn canonical_image(os: String, version: Option<String>) -> Result<String> {
cua_image::canonical::canonical_image(&os, version.as_deref()).map_err(image_error)
}
#[uniffi::export]
pub fn canonical_image_tier(
os: String,
version: Option<String>,
tier: Option<String>,
) -> Result<String> {
cua_image::canonical::tier_image(&os, version.as_deref(), tier.as_deref()).map_err(image_error)
}
#[uniffi::export]
pub fn omarchy_image(channel: Option<String>) -> Result<String> {
cua_image::canonical::omarchy_image(channel.as_deref()).map_err(image_error)
}
#[uniffi::export]
pub fn image_alias(name: String) -> Option<String> {
cua_image::canonical::alias(&name)
}
fn host_arch() -> &'static str {
if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"amd64"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_uses_the_installed_registry() {
let mut r = cua_image::testing::FakeRegistry::default();
let disk = r.index(
"ghcr.io/trycua/linux:24.04-disk",
&["amd64", "arm64"],
true,
None,
);
r.index(
"ghcr.io/trycua/linux:24.04",
&["amd64", "arm64"],
false,
None,
);
let hub = r.index(
"docker.io/library/ubuntu:24.04",
&["amd64", "arm64"],
false,
None,
);
cua_image::resolve::set_source(Some(std::sync::Arc::new(r)));
unsafe { std::env::remove_var("CUA_IMAGE_LINUX") };
let got = resolve_image(
"ghcr.io/trycua/linux:24.04".into(),
"vm".into(),
Some("arm64".into()),
)
.unwrap();
assert_eq!(got.variant, "containerdisk");
assert_eq!(got.pinned_ref, format!("ghcr.io/trycua/linux@{disk}"));
let got = resolve_image("ubuntu:24.04".into(), "container".into(), None).unwrap();
assert_eq!(got.pinned_ref, format!("docker.io/library/ubuntu@{hub}"));
assert!(resolve_image("linux".into(), "local".into(), None).is_err());
let e = resolve_image("nope:1".into(), "local".into(), None).unwrap_err();
assert!(matches!(e, CuaError::NotFound(_)), "{e:?}");
assert!(matches!(
resolve_image("x".into(), "kvm".into(), None),
Err(CuaError::InvalidArgument(_))
));
cua_image::resolve::set_source(None);
assert_eq!(
normalize_image("python:3.12-slim".into()).unwrap(),
"docker.io/library/python:3.12-slim"
);
assert_eq!(
canonical_image_tier("linux".into(), None, None).unwrap(),
"ghcr.io/trycua/linux:24.04"
);
assert!(matches!(
canonical_image_tier("linux".into(), None, Some("tiny".into())),
Err(CuaError::InvalidArgument(_))
));
assert!(matches!(
canonical_image_tier("windows".into(), None, Some("xcode".into())),
Err(CuaError::InvalidArgument(_))
));
let published = |r: &str| cua_image::catalog::find(r).is_none_or(|e| e.published);
for (got, want) in [
(
canonical_image_tier("linux".into(), None, Some("slim".into())),
"ghcr.io/trycua/linux:24.04-slim",
),
(
canonical_image_tier("macos".into(), None, Some("xcode".into())),
"ghcr.io/trycua/macos:26-xcode",
),
(omarchy_image(None), "ghcr.io/trycua/omarchy:edge"),
] {
if published(want) {
assert_eq!(got.unwrap(), want);
} else {
let e = got.unwrap_err();
assert!(
matches!(&e, CuaError::ImageNotPublished(m) if m.contains(want)),
"{e:?}"
);
}
}
assert_eq!(image_alias("python".into()), None);
assert_eq!(image_alias("ubuntu:24.04".into()), None);
}
}