#![allow(clippy::items_after_test_module)]
mod commit;
pub use commit::{
build_image_config_bytes, build_manifest_bytes, ImageConfigBuilder,
OCI_IMAGE_CONFIG_MEDIA_TYPE, OCI_IMAGE_MANIFEST_MEDIA_TYPE, OCI_WINDOWS_LAYER_MEDIA_TYPE,
};
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use async_trait::async_trait;
use crate::builder::{BuildOptions, BuiltImage, RegistryAuth};
use crate::dockerfile::Dockerfile;
use crate::error::{BuildError, Result};
use crate::tui::BuildEvent;
use crate::windows_builder::{WindowsBuildConfig, WindowsBuilder};
use super::BuildBackend;
fn default_storage_root() -> PathBuf {
if let Some(dir) = dirs::data_local_dir() {
dir.join("zlayer").join("builder-hcs")
} else {
PathBuf::from(r"C:\ProgramData\zlayer\builder-hcs")
}
}
pub struct HcsBackend {
storage_root: PathBuf,
}
impl std::fmt::Debug for HcsBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HcsBackend")
.field("storage_root", &self.storage_root)
.finish_non_exhaustive()
}
}
impl HcsBackend {
pub async fn new() -> Result<Self> {
let root = default_storage_root();
Self::with_storage_root(root).await
}
#[allow(clippy::unused_async)]
pub async fn with_storage_root(storage_root: PathBuf) -> Result<Self> {
std::fs::create_dir_all(&storage_root).map_err(|e| BuildError::ContextRead {
path: storage_root.clone(),
source: e,
})?;
Ok(Self { storage_root })
}
fn build_dir(&self, build_id: &str) -> PathBuf {
self.storage_root.join("builds").join(build_id)
}
}
#[async_trait]
impl BuildBackend for HcsBackend {
async fn build_image(
&self,
context: &Path,
dockerfile: &Dockerfile,
options: &BuildOptions,
event_tx: Option<mpsc::Sender<BuildEvent>>,
) -> Result<BuiltImage> {
let build_id = new_build_id();
let cache_dir = self.build_dir(&build_id);
std::fs::create_dir_all(&cache_dir).map_err(|e| BuildError::ContextRead {
path: cache_dir.clone(),
source: e,
})?;
let registry_auth = match options.registry_auth.as_ref() {
Some(a) => zlayer_registry::RegistryAuth::Basic(a.username.clone(), a.password.clone()),
None => zlayer_registry::RegistryAuth::Anonymous,
};
let cfg = WindowsBuildConfig {
cache_dir,
registry_auth,
platform: WindowsBuildConfig::default_platform().to_string(),
os_version_override: None,
scratch_size_gb: WindowsBuildConfig::default_scratch_size_gb(),
};
let builder = WindowsBuilder::new(cfg);
builder
.build_image_for_backend(context, dockerfile, options, event_tx.as_ref())
.await
}
async fn push_image(&self, _tag: &str, _auth: Option<&RegistryAuth>) -> Result<()> {
Err(BuildError::NotSupported {
operation: "HCS backend standalone push by tag — push is performed inline by \
build_image when options.push is set; standalone push without a \
rebuild is not yet wired (TODO(W-followup))"
.to_string(),
})
}
async fn tag_image(&self, _image: &str, _new_tag: &str) -> Result<()> {
Err(BuildError::NotSupported {
operation: "HCS backend retag — tags are embedded in the OCI index annotations at \
build time; a standalone retag lands with the push path \
(TODO(W-followup))"
.to_string(),
})
}
async fn manifest_create(&self, _name: &str) -> Result<()> {
Err(BuildError::NotSupported {
operation: "HCS backend manifest create — manifest lists are buildah-specific; the \
HCS builder produces a single platform-specific image. Use a Linux peer \
with buildah for multi-platform manifest composition."
.to_string(),
})
}
async fn manifest_add(&self, _manifest: &str, _image: &str) -> Result<()> {
Err(BuildError::NotSupported {
operation: "HCS backend manifest add — see manifest_create for rationale".to_string(),
})
}
async fn manifest_push(&self, _name: &str, _destination: &str) -> Result<()> {
Err(BuildError::NotSupported {
operation: "HCS backend manifest push — see manifest_create for rationale".to_string(),
})
}
async fn is_available(&self) -> bool {
true
}
fn name(&self) -> &'static str {
"hcs"
}
}
fn new_build_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let pid = u128::from(std::process::id());
let count = u128::from(COUNTER.fetch_add(1, Ordering::Relaxed));
let mixed = nanos ^ (pid.rotate_left(17)) ^ count.rotate_left(33);
format!("{:012x}", mixed & 0xFFFF_FFFF_FFFF)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_id_is_unique_and_hex_shaped() {
let a = new_build_id();
let b = new_build_id();
assert_ne!(a, b);
assert_eq!(a.len(), 12);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn default_storage_root_ends_in_zlayer_builder_hcs() {
let root = default_storage_root();
let s = root.to_string_lossy().to_ascii_lowercase();
assert!(s.contains("zlayer"));
assert!(s.contains("builder-hcs"));
}
}