use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[cfg(native_cache)]
use crate::persistence::Database;
#[cfg(native_cache)]
const MANIFEST_KEY: &str = "manifest";
pub const MANIFEST_SCHEMA: u32 = 1;
#[derive(Debug)]
pub enum BundleError {
#[cfg(native_cache)]
Io(std::io::Error),
#[cfg(native_cache)]
Database(rusqlite::Error),
NotABundle,
InvalidManifest(String),
UnsupportedSchema(u32),
TooLarge,
Flat(super::EmbeddedBundleError),
}
impl core::fmt::Display for BundleError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
#[cfg(native_cache)]
BundleError::Io(err) => write!(f, "bundle io error: {err}"),
#[cfg(native_cache)]
BundleError::Database(err) => write!(f, "bundle database error: {err}"),
BundleError::NotABundle => write!(f, "the file carries no bundle manifest"),
BundleError::InvalidManifest(err) => write!(f, "invalid bundle manifest: {err}"),
BundleError::UnsupportedSchema(schema) => {
write!(
f,
"unsupported bundle schema {schema} (this build supports {MANIFEST_SCHEMA})"
)
}
BundleError::TooLarge => write!(
f,
"the flat bundle format addresses at most {} bytes; export fewer namespaces",
u32::MAX
),
BundleError::Flat(err) => write!(f, "{err}"),
}
}
}
impl core::error::Error for BundleError {}
impl From<super::EmbeddedBundleError> for BundleError {
fn from(err: super::EmbeddedBundleError) -> Self {
Self::Flat(err)
}
}
#[cfg(native_cache)]
impl From<std::io::Error> for BundleError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
#[cfg(native_cache)]
impl From<rusqlite::Error> for BundleError {
fn from(err: rusqlite::Error) -> Self {
Self::Database(err)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BundleManifest {
pub schema: u32,
pub name: String,
pub cubecl_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_unix_secs: Option<u64>,
#[serde(default, rename = "environments")]
pub environments: Vec<EnvironmentInfo>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EnvironmentInfo {
#[serde(default)]
pub label: String,
#[serde(default)]
pub os: String,
#[serde(default)]
pub arch: String,
#[serde(default)]
pub devices: Vec<String>,
}
impl BundleManifest {
pub fn parse(content: &[u8]) -> Result<Self, BundleError> {
if content.is_empty() {
return Err(BundleError::NotABundle);
}
let manifest: Self = serde_json::from_slice(content)
.map_err(|err| BundleError::InvalidManifest(err.to_string()))?;
if manifest.schema != MANIFEST_SCHEMA {
return Err(BundleError::UnsupportedSchema(manifest.schema));
}
Ok(manifest)
}
pub fn warn_on_version_mismatch(&self) {
if self.cubecl_version != env!("CARGO_PKG_VERSION") {
log::warn!(
"Bundle '{}' was built for cubecl {}, running {}; its entries will be ignored.",
self.name,
self.cubecl_version,
env!("CARGO_PKG_VERSION"),
);
}
}
#[cfg(native_cache)]
pub fn read(database: &Database) -> Result<Self, BundleError> {
let content = read_meta(database, MANIFEST_KEY)?.ok_or(BundleError::NotABundle)?;
Self::parse(content.as_bytes())
}
#[cfg(native_cache)]
pub fn write(&self, database: &Database) -> Result<(), BundleError> {
let content = serde_json::to_string_pretty(self)
.map_err(|err| BundleError::InvalidManifest(err.to_string()))?;
database.with_connection(|conn| {
crate::persistence::sqlite::meta_set(conn, MANIFEST_KEY, &content)
})?;
Ok(())
}
}
#[cfg(native_cache)]
fn read_meta(database: &Database, key: &str) -> Result<Option<String>, BundleError> {
let content = database.with_connection(|conn| crate::persistence::sqlite::meta_get(conn, key));
match content {
Ok(content) => Ok(content),
Err(err) if is_missing_meta(&err) => Err(BundleError::NotABundle),
Err(err) => Err(BundleError::Database(err)),
}
}
#[cfg(native_cache)]
fn is_missing_meta(err: &rusqlite::Error) -> bool {
match err {
rusqlite::Error::SqliteFailure(err, _) if err.code == rusqlite::ErrorCode::NotADatabase => {
true
}
rusqlite::Error::SqliteFailure(_, Some(message))
| rusqlite::Error::SqlInputError { msg: message, .. } => message.contains("no such table"),
_ => false,
}
}