xbp 10.43.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
//! Map CLI `XbpConfig` → engine `ProjectConfig` (pure data view).

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;

use crate::strategies::XbpConfig;
use crate::utils::resolve_cargo_package_version;
use xbp_deploy::{
    DeployGroupView, KubernetesDefaults, ProjectConfig, ServiceConfigView, ServiceDeployEnvView,
    ServiceDeployView, ServiceK8sView, ServiceOciView,
};

pub fn map_project_config(project_root: &Path, config: &XbpConfig) -> ProjectConfig {
    let history_dir = config
        .deploy
        .as_ref()
        .and_then(|d| d.history_dir.as_deref())
        .filter(|s| !s.is_empty())
        .unwrap_or(".xbp/deployments");
    let lock_file = config
        .deploy
        .as_ref()
        .and_then(|d| d.lock_file.as_deref())
        .filter(|s| !s.is_empty())
        .unwrap_or(".xbp/deploy-lock.json");

    let mut groups = HashMap::new();
    if let Some(deploy) = &config.deploy {
        for (name, g) in &deploy.groups {
            groups.insert(
                name.clone(),
                DeployGroupView {
                    description: g.description.clone(),
                    services: g.services.clone(),
                    order: g.order.clone(),
                },
            );
        }
    }

    let services = config
        .services
        .as_ref()
        .map(|list| {
            list.iter()
                .map(|s| {
                    let version = resolve_service_version(project_root, config, s);
                    ServiceConfigView {
                        name: s.name.clone(),
                        root_directory: s.root_directory.clone(),
                        version,
                        depends_on: s.depends_on.clone().unwrap_or_default(),
                        oci: s.oci.as_ref().and_then(|o| {
                            let image = o.image.as_ref()?.trim();
                            if image.is_empty() {
                                return None;
                            }
                            Some(ServiceOciView {
                                image: image.to_string(),
                                tag: o.tag.clone(),
                                tag_from: o.tag_from.clone(),
                                dockerfile: o.dockerfile.clone(),
                                platforms: o.platforms.clone(),
                            })
                        }),
                        deploy: s.deploy.as_ref().map(|d| ServiceDeployView {
                            provider: d
                                .provider
                                .clone()
                                .unwrap_or_else(|| "kubernetes".into()),
                            envs: d
                                .envs
                                .iter()
                                .map(|(k, v)| {
                                    (
                                        k.clone(),
                                        ServiceDeployEnvView {
                                            namespace: v.namespace.clone(),
                                            replicas: v.replicas,
                                            health: v.health.clone(),
                                            kubernetes: v.kubernetes.as_ref().map(|k8s| {
                                                ServiceK8sView {
                                                    manifests_base: k8s
                                                        .manifests
                                                        .as_ref()
                                                        .and_then(|m| m.base.clone()),
                                                    manifests_overlay: k8s
                                                        .manifests
                                                        .as_ref()
                                                        .and_then(|m| m.overlay.clone()),
                                                    workload: k8s.workload.clone(),
                                                    service: k8s.service.clone(),
                                                    crds_path: k8s.crds_path.clone(),
                                                    install_path: k8s.install_path.clone(),
                                                    selector: k8s.selector.clone(),
                                                }
                                            }),
                                        },
                                    )
                                })
                                .collect(),
                        }),
                    }
                })
                .collect()
        })
        .unwrap_or_default();

    ProjectConfig {
        project_name: config.project_name.clone(),
        version: config.version.clone(),
        project_root: project_root.to_path_buf(),
        services,
        groups,
        default_env: config
            .deploy
            .as_ref()
            .and_then(|d| d.default_env.clone()),
        kubernetes: config.kubernetes.as_ref().map(|k| KubernetesDefaults {
            default_context: k.default_context.clone(),
            default_namespace: k.default_namespace.clone(),
            require_context_confirmation_for_apply: k
                .require_context_confirmation_for_apply
                .unwrap_or(true),
            kubeconfig: k.kubeconfig.clone(),
        }),
        history_dir: project_root.join(history_dir),
        lock_file: project_root.join(lock_file),
        git_sha: resolve_git_sha(project_root),
    }
}

fn resolve_service_version(
    project_root: &Path,
    config: &XbpConfig,
    service: &crate::strategies::ServiceConfig,
) -> Option<String> {
    if let Some(targets) = service.version_targets.as_ref() {
        for target in targets {
            let path = project_root.join(target);
            if !path.exists() {
                continue;
            }
            let path_str = path.to_string_lossy();
            if path_str.ends_with("Cargo.toml") {
                if let Ok(Some(v)) = resolve_cargo_package_version(&path) {
                    return Some(v);
                }
            } else if path_str.ends_with("package.json") {
                if let Ok(raw) = std::fs::read_to_string(&path) {
                    if let Ok(val) = serde_json::from_str::<serde_json::Value>(&raw) {
                        if let Some(v) = val.get("version").and_then(|v| v.as_str()) {
                            return Some(v.to_string());
                        }
                    }
                }
            }
        }
    }
    Some(config.version.clone())
}

fn resolve_git_sha(project_root: &Path) -> Option<String> {
    let output = Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .current_dir(project_root)
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let sha = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if sha.is_empty() {
        None
    } else {
        Some(sha)
    }
}

#[allow(dead_code)]
pub fn default_lock_path(project_root: &Path) -> PathBuf {
    project_root.join(".xbp/deploy-lock.json")
}