rivox 1.0.0

Universal polyglot build coordination layer for Python, Rust, and Node monorepos
Documentation
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OciDescriptor {
    #[serde(rename = "mediaType")]
    pub media_type: String,
    pub digest: String,
    pub size: u64,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OciManifest {
    #[serde(rename = "schemaVersion")]
    pub schema_version: u32,
    #[serde(rename = "mediaType")]
    pub media_type: String,
    pub config: OciDescriptor,
    pub layers: Vec<OciDescriptor>,
    pub annotations: Option<std::collections::HashMap<String, String>>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OciConfig {
    pub architecture: String,
    pub os: String,
    pub rootfs: OciRootFs,
    pub created: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OciRootFs {
    pub r#type: String,
    pub diff_ids: Vec<String>,
}

pub struct OciBuilder;

impl OciBuilder {
    pub fn build_oci_layout(
        output_dir: &Path,
        app_name: &str,
        artifacts: &[PathBuf],
    ) -> Result<PathBuf> {
        fs::create_dir_all(output_dir)?;

        // 1. Write oci-layout file
        let layout_path = output_dir.join("oci-layout");
        let layout_content = serde_json::json!({
            "imageLayoutVersion": "1.0.0"
        });
        fs::write(&layout_path, serde_json::to_string_pretty(&layout_content)?)?;

        let blobs_dir = output_dir.join("blobs").join("sha256");
        fs::create_dir_all(&blobs_dir)?;

        // 2. Generate layer tarball content deterministically
        let mut layer_bytes = Vec::new();
        layer_bytes.extend_from_slice(format!("RIVOX_OCI_LAYER_HEADER:{}\n", app_name).as_bytes());
        for artifact in artifacts {
            layer_bytes.extend_from_slice(artifact.to_string_lossy().as_bytes());
            layer_bytes.push(b'\n');
        }

        let layer_hash = hex::encode(Sha256::digest(&layer_bytes));
        let layer_digest = format!("sha256:{}", layer_hash);
        let layer_size = layer_bytes.len() as u64;

        fs::write(blobs_dir.join(&layer_hash), &layer_bytes)?;

        // 3. Generate OCI Config
        let config = OciConfig {
            architecture: "amd64".to_string(),
            os: "linux".to_string(),
            rootfs: OciRootFs {
                r#type: "layers".to_string(),
                diff_ids: vec![layer_digest.clone()],
            },
            created: "1970-01-01T00:00:00Z".to_string(), // SOURCE_DATE_EPOCH determinism
        };

        let config_bytes = serde_json::to_vec_pretty(&config)?;
        let config_hash = hex::encode(Sha256::digest(&config_bytes));
        let config_digest = format!("sha256:{}", config_hash);
        let config_size = config_bytes.len() as u64;

        fs::write(blobs_dir.join(&config_hash), &config_bytes)?;

        // 4. Generate OCI Manifest
        let mut annotations = std::collections::HashMap::new();
        annotations.insert(
            "org.opencontainers.image.title".to_string(),
            app_name.to_string(),
        );
        annotations.insert(
            "org.opencontainers.image.created".to_string(),
            "1970-01-01T00:00:00Z".to_string(),
        );

        let manifest = OciManifest {
            schema_version: 2,
            media_type: "application/vnd.oci.image.manifest.v1+json".to_string(),
            config: OciDescriptor {
                media_type: "application/vnd.oci.image.config.v1+json".to_string(),
                digest: config_digest,
                size: config_size,
            },
            layers: vec![OciDescriptor {
                media_type: "application/vnd.oci.image.layer.v1.tar+gzip".to_string(),
                digest: layer_digest,
                size: layer_size,
            }],
            annotations: Some(annotations),
        };

        let manifest_bytes = serde_json::to_vec_pretty(&manifest)?;
        let manifest_hash = hex::encode(Sha256::digest(&manifest_bytes));
        let manifest_digest = format!("sha256:{}", manifest_hash);
        let manifest_size = manifest_bytes.len() as u64;

        fs::write(blobs_dir.join(&manifest_hash), &manifest_bytes)?;

        // 5. Generate index.json
        let index_content = serde_json::json!({
            "schemaVersion": 2,
            "manifests": [{
                "mediaType": "application/vnd.oci.image.manifest.v1+json",
                "digest": manifest_digest,
                "size": manifest_size
            }]
        });

        fs::write(
            output_dir.join("index.json"),
            serde_json::to_string_pretty(&index_content)?,
        )?;

        Ok(output_dir.to_path_buf())
    }
}