use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::sync::Arc;
use tokio::fs;
use tracing::{debug, info, instrument};
use crate::backend::BuildBackend;
use crate::buildah::BuildahExecutor;
use crate::dockerfile::{Dockerfile, RunMount};
use crate::error::{BuildError, Result};
use crate::templates::{get_template, Runtime};
use crate::tui::BuildEvent;
#[cfg(feature = "cache")]
use zlayer_registry::cache::BlobCacheBackend;
#[cfg(feature = "local-registry")]
use zlayer_registry::LocalRegistry;
#[derive(Debug)]
pub enum BuildOutput {
Dockerfile(Dockerfile),
WasmArtifact {
wasm_path: PathBuf,
oci_path: Option<PathBuf>,
language: String,
optimized: bool,
size: u64,
},
}
#[cfg(feature = "cache")]
#[derive(Debug, Clone, Default)]
pub enum CacheBackendConfig {
#[default]
Memory,
#[cfg(feature = "cache-persistent")]
Persistent {
path: PathBuf,
},
#[cfg(feature = "cache-s3")]
S3 {
bucket: String,
region: Option<String>,
endpoint: Option<String>,
prefix: Option<String>,
},
}
#[derive(Debug, Clone)]
pub struct BuiltImage {
pub image_id: String,
pub tags: Vec<String>,
pub layer_count: usize,
pub size: u64,
pub build_time_ms: u64,
pub is_manifest: bool,
}
#[derive(Debug, Clone)]
pub struct RegistryAuth {
pub username: String,
pub password: String,
}
impl RegistryAuth {
pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
Self {
username: username.into(),
password: password.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PullBaseMode {
#[default]
Newer,
Always,
Never,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct BuildOptions {
pub dockerfile: Option<PathBuf>,
pub zimagefile: Option<PathBuf>,
pub runtime: Option<Runtime>,
pub build_args: HashMap<String, String>,
pub target: Option<String>,
pub tags: Vec<String>,
pub no_cache: bool,
pub push: bool,
pub registry_auth: Option<RegistryAuth>,
pub squash: bool,
pub format: Option<String>,
pub layers: bool,
pub cache_from: Option<String>,
pub cache_to: Option<String>,
pub cache_ttl: Option<std::time::Duration>,
#[cfg(feature = "cache")]
pub cache_backend_config: Option<CacheBackendConfig>,
pub default_registry: Option<String>,
pub default_cache_mounts: Vec<RunMount>,
pub retries: u32,
pub platform: Option<String>,
pub source_hash: Option<String>,
pub pull: PullBaseMode,
}
impl Default for BuildOptions {
fn default() -> Self {
Self {
dockerfile: None,
zimagefile: None,
runtime: None,
build_args: HashMap::new(),
target: None,
tags: Vec::new(),
no_cache: false,
push: false,
registry_auth: None,
squash: false,
format: None,
layers: true,
cache_from: None,
cache_to: None,
cache_ttl: None,
#[cfg(feature = "cache")]
cache_backend_config: None,
default_registry: None,
default_cache_mounts: Vec::new(),
retries: 0,
platform: None,
source_hash: None,
pull: PullBaseMode::default(),
}
}
}
pub struct ImageBuilder {
context: PathBuf,
options: BuildOptions,
#[allow(dead_code)]
executor: BuildahExecutor,
event_tx: Option<mpsc::Sender<BuildEvent>>,
backend: Option<Arc<dyn BuildBackend>>,
#[cfg(feature = "cache")]
cache_backend: Option<Arc<Box<dyn BlobCacheBackend>>>,
#[cfg(feature = "local-registry")]
local_registry: Option<LocalRegistry>,
}
impl ImageBuilder {
#[instrument(skip_all, fields(context = %context.as_ref().display()))]
pub async fn new(context: impl AsRef<Path>) -> Result<Self> {
let context = context.as_ref().to_path_buf();
if !context.exists() {
return Err(BuildError::ContextRead {
path: context,
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"Build context directory not found",
),
});
}
let backend = crate::backend::detect_backend().await.ok();
let executor = match BuildahExecutor::new_async().await {
Ok(exec) => exec,
#[cfg(target_os = "macos")]
Err(_) => {
info!("Buildah not found on macOS; backend will handle build dispatch");
BuildahExecutor::default()
}
#[cfg(not(target_os = "macos"))]
Err(e) => return Err(e),
};
debug!("Created ImageBuilder for context: {}", context.display());
Ok(Self {
context,
options: BuildOptions::default(),
executor,
event_tx: None,
backend,
#[cfg(feature = "cache")]
cache_backend: None,
#[cfg(feature = "local-registry")]
local_registry: None,
})
}
pub fn with_executor(context: impl AsRef<Path>, executor: BuildahExecutor) -> Result<Self> {
let context = context.as_ref().to_path_buf();
if !context.exists() {
return Err(BuildError::ContextRead {
path: context,
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"Build context directory not found",
),
});
}
let backend: Arc<dyn BuildBackend> = Arc::new(
crate::backend::BuildahBackend::with_executor(executor.clone()),
);
Ok(Self {
context,
options: BuildOptions::default(),
executor,
event_tx: None,
backend: Some(backend),
#[cfg(feature = "cache")]
cache_backend: None,
#[cfg(feature = "local-registry")]
local_registry: None,
})
}
pub fn with_backend(context: impl AsRef<Path>, backend: Arc<dyn BuildBackend>) -> Result<Self> {
let context = context.as_ref().to_path_buf();
if !context.exists() {
return Err(BuildError::ContextRead {
path: context,
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"Build context directory not found",
),
});
}
Ok(Self {
context,
options: BuildOptions::default(),
executor: BuildahExecutor::default(),
event_tx: None,
backend: Some(backend),
#[cfg(feature = "cache")]
cache_backend: None,
#[cfg(feature = "local-registry")]
local_registry: None,
})
}
#[must_use]
pub fn dockerfile(mut self, path: impl AsRef<Path>) -> Self {
self.options.dockerfile = Some(path.as_ref().to_path_buf());
self
}
#[must_use]
pub fn zimagefile(mut self, path: impl AsRef<Path>) -> Self {
self.options.zimagefile = Some(path.as_ref().to_path_buf());
self
}
#[must_use]
pub fn runtime(mut self, runtime: Runtime) -> Self {
self.options.runtime = Some(runtime);
self
}
#[must_use]
pub fn build_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.build_args.insert(key.into(), value.into());
self
}
#[must_use]
pub fn build_args(mut self, args: HashMap<String, String>) -> Self {
self.options.build_args.extend(args);
self
}
#[must_use]
pub fn target(mut self, stage: impl Into<String>) -> Self {
self.options.target = Some(stage.into());
self
}
#[must_use]
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.options.tags.push(tag.into());
self
}
#[must_use]
pub fn no_cache(mut self) -> Self {
self.options.no_cache = true;
self
}
#[must_use]
pub fn pull(mut self, mode: PullBaseMode) -> Self {
self.options.pull = mode;
self
}
#[must_use]
pub fn layers(mut self, enable: bool) -> Self {
self.options.layers = enable;
self
}
#[must_use]
pub fn cache_from(mut self, registry: impl Into<String>) -> Self {
self.options.cache_from = Some(registry.into());
self
}
#[must_use]
pub fn cache_to(mut self, registry: impl Into<String>) -> Self {
self.options.cache_to = Some(registry.into());
self
}
#[must_use]
pub fn cache_ttl(mut self, ttl: std::time::Duration) -> Self {
self.options.cache_ttl = Some(ttl);
self
}
#[must_use]
pub fn push(mut self, auth: RegistryAuth) -> Self {
self.options.push = true;
self.options.registry_auth = Some(auth);
self
}
#[must_use]
pub fn push_without_auth(mut self) -> Self {
self.options.push = true;
self.options.registry_auth = None;
self
}
#[must_use]
pub fn default_registry(mut self, registry: impl Into<String>) -> Self {
self.options.default_registry = Some(registry.into());
self
}
#[cfg(feature = "local-registry")]
#[must_use]
pub fn with_local_registry(mut self, registry: LocalRegistry) -> Self {
self.local_registry = Some(registry);
self
}
#[must_use]
pub fn squash(mut self) -> Self {
self.options.squash = true;
self
}
#[must_use]
pub fn format(mut self, format: impl Into<String>) -> Self {
self.options.format = Some(format.into());
self
}
#[must_use]
pub fn default_cache_mounts(mut self, mounts: Vec<RunMount>) -> Self {
self.options.default_cache_mounts = mounts;
self
}
#[must_use]
pub fn retries(mut self, retries: u32) -> Self {
self.options.retries = retries;
self
}
#[must_use]
pub fn platform(mut self, platform: impl Into<String>) -> Self {
self.options.platform = Some(platform.into());
self
}
#[must_use]
pub fn source_hash(mut self, hash: impl Into<String>) -> Self {
self.options.source_hash = Some(hash.into());
self
}
#[must_use]
pub fn with_events(mut self, tx: mpsc::Sender<BuildEvent>) -> Self {
self.event_tx = Some(tx);
self
}
#[cfg(feature = "cache-persistent")]
#[must_use]
pub fn with_cache_dir(mut self, path: impl AsRef<Path>) -> Self {
self.options.cache_backend_config = Some(CacheBackendConfig::Persistent {
path: path.as_ref().to_path_buf(),
});
debug!(
"Configured persistent cache at: {}",
path.as_ref().display()
);
self
}
#[cfg(feature = "cache")]
#[must_use]
pub fn with_memory_cache(mut self) -> Self {
self.options.cache_backend_config = Some(CacheBackendConfig::Memory);
debug!("Configured in-memory cache");
self
}
#[cfg(feature = "cache-s3")]
#[must_use]
pub fn with_s3_cache(mut self, bucket: impl Into<String>, region: Option<String>) -> Self {
self.options.cache_backend_config = Some(CacheBackendConfig::S3 {
bucket: bucket.into(),
region,
endpoint: None,
prefix: None,
});
debug!("Configured S3 cache");
self
}
#[cfg(feature = "cache-s3")]
#[must_use]
pub fn with_s3_cache_endpoint(
mut self,
bucket: impl Into<String>,
endpoint: impl Into<String>,
region: Option<String>,
) -> Self {
self.options.cache_backend_config = Some(CacheBackendConfig::S3 {
bucket: bucket.into(),
region,
endpoint: Some(endpoint.into()),
prefix: None,
});
debug!("Configured S3 cache with custom endpoint");
self
}
#[cfg(feature = "cache")]
#[must_use]
pub fn with_cache_config(mut self, config: CacheBackendConfig) -> Self {
self.options.cache_backend_config = Some(config);
debug!("Configured custom cache backend");
self
}
#[cfg(feature = "cache")]
#[must_use]
pub fn with_cache_backend(mut self, backend: Arc<Box<dyn BlobCacheBackend>>) -> Self {
self.cache_backend = Some(backend);
debug!("Configured pre-initialized cache backend");
self
}
#[instrument(skip(self), fields(context = %self.context.display()))]
pub async fn build(self) -> Result<BuiltImage> {
let start_time = std::time::Instant::now();
info!("Starting build in context: {}", self.context.display());
let build_output = self.get_build_output().await?;
if let BuildOutput::WasmArtifact {
wasm_path,
oci_path: _,
language,
optimized,
size,
} = build_output
{
#[allow(clippy::cast_possible_truncation)]
let build_time_ms = start_time.elapsed().as_millis() as u64;
self.send_event(BuildEvent::BuildComplete {
image_id: wasm_path.display().to_string(),
});
info!(
"WASM build completed in {}ms: {} ({}, {} bytes, optimized={})",
build_time_ms,
wasm_path.display(),
language,
size,
optimized
);
return Ok(BuiltImage {
image_id: format!("wasm:{}", wasm_path.display()),
tags: self.options.tags.clone(),
layer_count: 1,
size,
build_time_ms,
is_manifest: false,
});
}
let BuildOutput::Dockerfile(dockerfile) = build_output else {
unreachable!("WasmArtifact case handled above");
};
debug!("Parsed Dockerfile with {} stages", dockerfile.stages.len());
let backend = self
.backend
.as_ref()
.ok_or_else(|| BuildError::BuildahNotFound {
message: "No build backend configured".into(),
})?;
info!("Delegating build to {} backend", backend.name());
backend
.build_image(
&self.context,
&dockerfile,
&self.options,
self.event_tx.clone(),
)
.await
}
async fn get_build_output(&self) -> Result<BuildOutput> {
if let Some(runtime) = &self.options.runtime {
debug!("Using runtime template: {}", runtime);
let content = get_template(*runtime);
return Ok(BuildOutput::Dockerfile(Dockerfile::parse(content)?));
}
if let Some(ref zimage_path) = self.options.zimagefile {
debug!("Reading ZImagefile: {}", zimage_path.display());
let content =
fs::read_to_string(zimage_path)
.await
.map_err(|e| BuildError::ContextRead {
path: zimage_path.clone(),
source: e,
})?;
let zimage = crate::zimage::parse_zimagefile(&content)?;
return self.handle_zimage(&zimage).await;
}
let auto_zimage_path = self.context.join("ZImagefile");
if auto_zimage_path.exists() {
debug!(
"Found ZImagefile in context: {}",
auto_zimage_path.display()
);
let content = fs::read_to_string(&auto_zimage_path).await.map_err(|e| {
BuildError::ContextRead {
path: auto_zimage_path,
source: e,
}
})?;
let zimage = crate::zimage::parse_zimagefile(&content)?;
return self.handle_zimage(&zimage).await;
}
let dockerfile_path = self
.options
.dockerfile
.clone()
.unwrap_or_else(|| self.context.join("Dockerfile"));
debug!("Reading Dockerfile: {}", dockerfile_path.display());
let content =
fs::read_to_string(&dockerfile_path)
.await
.map_err(|e| BuildError::ContextRead {
path: dockerfile_path,
source: e,
})?;
Ok(BuildOutput::Dockerfile(Dockerfile::parse(&content)?))
}
async fn handle_zimage(&self, zimage: &crate::zimage::ZImage) -> Result<BuildOutput> {
if let Some(ref runtime_name) = zimage.runtime {
let rt = Runtime::from_name(runtime_name).ok_or_else(|| {
BuildError::zimagefile_validation(format!(
"unknown runtime '{runtime_name}' in ZImagefile"
))
})?;
let content = get_template(rt);
return Ok(BuildOutput::Dockerfile(Dockerfile::parse(content)?));
}
if let Some(ref wasm_config) = zimage.wasm {
return self.handle_wasm_build(wasm_config).await;
}
let resolved = self.resolve_build_directives(zimage).await?;
Ok(BuildOutput::Dockerfile(
crate::zimage::zimage_to_dockerfile(&resolved)?,
))
}
async fn handle_wasm_build(
&self,
wasm_config: &crate::zimage::ZWasmConfig,
) -> Result<BuildOutput> {
use crate::wasm_builder::{build_wasm, WasiTarget, WasmBuildConfig, WasmLanguage};
info!("ZImagefile specifies WASM mode, running WASM build");
let target = match wasm_config.target.as_str() {
"preview1" => WasiTarget::Preview1,
_ => WasiTarget::Preview2,
};
let language = wasm_config
.language
.as_deref()
.and_then(WasmLanguage::from_name);
if let Some(ref lang_str) = wasm_config.language {
if language.is_none() {
return Err(BuildError::zimagefile_validation(format!(
"unknown WASM language '{lang_str}'. Supported: rust, go, python, \
typescript, assemblyscript, c, zig"
)));
}
}
let mut config = WasmBuildConfig {
language,
target,
optimize: wasm_config.optimize,
opt_level: wasm_config
.opt_level
.clone()
.unwrap_or_else(|| "Oz".to_string()),
wit_path: wasm_config.wit.as_ref().map(PathBuf::from),
output_path: wasm_config.output.as_ref().map(PathBuf::from),
world: wasm_config.world.clone(),
features: wasm_config.features.clone(),
build_args: wasm_config.build_args.clone(),
pre_build: Vec::new(),
post_build: Vec::new(),
adapter: wasm_config.adapter.as_ref().map(PathBuf::from),
};
for cmd in &wasm_config.pre_build {
config.pre_build.push(zcommand_to_args(cmd));
}
for cmd in &wasm_config.post_build {
config.post_build.push(zcommand_to_args(cmd));
}
let result = build_wasm(&self.context, config).await?;
let language_name = result.language.name().to_string();
let wasm_path = result.wasm_path;
let size = result.size;
info!(
"WASM build complete: {} ({} bytes, optimized={})",
wasm_path.display(),
size,
wasm_config.optimize
);
Ok(BuildOutput::WasmArtifact {
wasm_path,
oci_path: None,
language: language_name,
optimized: wasm_config.optimize,
size,
})
}
async fn resolve_build_directives(
&self,
zimage: &crate::zimage::ZImage,
) -> Result<crate::zimage::ZImage> {
let mut resolved = zimage.clone();
if let Some(ref build_ctx) = resolved.build {
let tag = self.run_nested_build(build_ctx, "toplevel").await?;
resolved.base = Some(tag);
resolved.build = None;
}
if let Some(ref mut stages) = resolved.stages {
for (name, stage) in stages.iter_mut() {
if let Some(ref build_ctx) = stage.build {
let tag = self.run_nested_build(build_ctx, name).await?;
stage.base = Some(tag);
stage.build = None;
}
}
}
Ok(resolved)
}
fn run_nested_build<'a>(
&'a self,
build_ctx: &'a crate::zimage::types::ZBuildContext,
stage_name: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
Box::pin(self.run_nested_build_inner(build_ctx, stage_name))
}
async fn run_nested_build_inner(
&self,
build_ctx: &crate::zimage::types::ZBuildContext,
stage_name: &str,
) -> Result<String> {
let context_dir = build_ctx.context_dir(&self.context);
if !context_dir.exists() {
return Err(BuildError::ContextRead {
path: context_dir,
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"build context directory not found for build directive in '{stage_name}'"
),
),
});
}
info!(
"Building nested image for '{}' from context: {}",
stage_name,
context_dir.display()
);
let tag = format!(
"zlayer-build-dep-{}:{}",
stage_name,
chrono_lite_timestamp()
);
let mut nested = ImageBuilder::new(&context_dir).await?;
nested = nested.tag(&tag);
if let Some(file) = build_ctx.file() {
let file_path = context_dir.join(file);
if std::path::Path::new(file).extension().is_some_and(|ext| {
ext.eq_ignore_ascii_case("yml") || ext.eq_ignore_ascii_case("yaml")
}) || file.starts_with("ZImagefile")
{
nested = nested.zimagefile(file_path);
} else {
nested = nested.dockerfile(file_path);
}
}
for (key, value) in build_ctx.args() {
nested = nested.build_arg(&key, &value);
}
if let Some(ref reg) = self.options.default_registry {
nested = nested.default_registry(reg.clone());
}
let result = nested.build().await?;
info!(
"Nested build for '{}' completed: {}",
stage_name, result.image_id
);
Ok(tag)
}
fn send_event(&self, event: BuildEvent) {
if let Some(tx) = &self.event_tx {
let _ = tx.send(event);
}
}
}
fn chrono_lite_timestamp() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
format!("{}", duration.as_secs())
}
fn zcommand_to_args(cmd: &crate::zimage::ZCommand) -> Vec<String> {
match cmd {
crate::zimage::ZCommand::Shell(s) => {
vec!["/bin/sh".to_string(), "-c".to_string(), s.clone()]
}
crate::zimage::ZCommand::Exec(args) => args.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_auth_new() {
let auth = RegistryAuth::new("user", "pass");
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "pass");
}
#[test]
fn test_build_options_default() {
let opts = BuildOptions::default();
assert!(opts.dockerfile.is_none());
assert!(opts.zimagefile.is_none());
assert!(opts.runtime.is_none());
assert!(opts.build_args.is_empty());
assert!(opts.target.is_none());
assert!(opts.tags.is_empty());
assert!(!opts.no_cache);
assert!(!opts.push);
assert!(!opts.squash);
assert!(opts.layers); assert!(opts.cache_from.is_none());
assert!(opts.cache_to.is_none());
assert!(opts.cache_ttl.is_none());
#[cfg(feature = "cache")]
assert!(opts.cache_backend_config.is_none());
}
fn create_test_builder() -> ImageBuilder {
ImageBuilder {
context: PathBuf::from("/tmp/test"),
options: BuildOptions::default(),
executor: BuildahExecutor::with_path("/usr/bin/buildah"),
event_tx: None,
backend: None,
#[cfg(feature = "cache")]
cache_backend: None,
#[cfg(feature = "local-registry")]
local_registry: None,
}
}
#[test]
fn test_builder_chaining() {
let mut builder = create_test_builder();
builder = builder
.dockerfile("./Dockerfile.test")
.runtime(Runtime::Node20)
.build_arg("VERSION", "1.0")
.target("builder")
.tag("myapp:latest")
.tag("myapp:v1")
.no_cache()
.squash()
.format("oci");
assert_eq!(
builder.options.dockerfile,
Some(PathBuf::from("./Dockerfile.test"))
);
assert_eq!(builder.options.runtime, Some(Runtime::Node20));
assert_eq!(
builder.options.build_args.get("VERSION"),
Some(&"1.0".to_string())
);
assert_eq!(builder.options.target, Some("builder".to_string()));
assert_eq!(builder.options.tags.len(), 2);
assert!(builder.options.no_cache);
assert!(builder.options.squash);
assert_eq!(builder.options.format, Some("oci".to_string()));
}
#[test]
fn test_builder_push_with_auth() {
let mut builder = create_test_builder();
builder = builder.push(RegistryAuth::new("user", "pass"));
assert!(builder.options.push);
assert!(builder.options.registry_auth.is_some());
let auth = builder.options.registry_auth.unwrap();
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "pass");
}
#[test]
fn test_builder_push_without_auth() {
let mut builder = create_test_builder();
builder = builder.push_without_auth();
assert!(builder.options.push);
assert!(builder.options.registry_auth.is_none());
}
#[test]
fn test_builder_layers() {
let mut builder = create_test_builder();
assert!(builder.options.layers);
builder = builder.layers(false);
assert!(!builder.options.layers);
builder = builder.layers(true);
assert!(builder.options.layers);
}
#[test]
fn test_builder_cache_from() {
let mut builder = create_test_builder();
assert!(builder.options.cache_from.is_none());
builder = builder.cache_from("registry.example.com/myapp:cache");
assert_eq!(
builder.options.cache_from,
Some("registry.example.com/myapp:cache".to_string())
);
}
#[test]
fn test_builder_cache_to() {
let mut builder = create_test_builder();
assert!(builder.options.cache_to.is_none());
builder = builder.cache_to("registry.example.com/myapp:cache");
assert_eq!(
builder.options.cache_to,
Some("registry.example.com/myapp:cache".to_string())
);
}
#[test]
fn test_builder_cache_ttl() {
use std::time::Duration;
let mut builder = create_test_builder();
assert!(builder.options.cache_ttl.is_none());
builder = builder.cache_ttl(Duration::from_secs(3600));
assert_eq!(builder.options.cache_ttl, Some(Duration::from_secs(3600)));
}
#[test]
fn test_builder_cache_options_chaining() {
use std::time::Duration;
let builder = create_test_builder()
.layers(true)
.cache_from("registry.example.com/cache:input")
.cache_to("registry.example.com/cache:output")
.cache_ttl(Duration::from_secs(7200))
.no_cache();
assert!(builder.options.layers);
assert_eq!(
builder.options.cache_from,
Some("registry.example.com/cache:input".to_string())
);
assert_eq!(
builder.options.cache_to,
Some("registry.example.com/cache:output".to_string())
);
assert_eq!(builder.options.cache_ttl, Some(Duration::from_secs(7200)));
assert!(builder.options.no_cache);
}
#[test]
fn test_chrono_lite_timestamp() {
let ts = chrono_lite_timestamp();
let parsed: u64 = ts.parse().expect("Should be a valid u64");
assert!(parsed > 1_700_000_000);
}
}