Skip to main content

packc/cli/
resolve.rs

1#![forbid(unsafe_code)]
2
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result, anyhow};
7use clap::Args;
8use greentic_pack::pack_lock::{LockedComponent, PackLockV1, write_pack_lock};
9use greentic_types::ComponentId;
10use greentic_types::flow_resolve_summary::{FlowResolveSummarySourceRefV1, FlowResolveSummaryV1};
11
12use crate::config::load_pack_config;
13use crate::flow_resolve::read_flow_resolve_summary_for_flow;
14use crate::runtime::RuntimeContext;
15
16#[derive(Debug, Args)]
17pub struct ResolveArgs {
18    /// Pack root directory containing pack.yaml.
19    #[arg(long = "in", value_name = "DIR", default_value = ".")]
20    pub input: PathBuf,
21
22    /// Output path for pack.lock.json (default: pack.lock.json under pack root).
23    #[arg(long = "lock", value_name = "FILE")]
24    pub lock: Option<PathBuf>,
25}
26
27pub async fn handle(args: ResolveArgs, runtime: &RuntimeContext, emit_path: bool) -> Result<()> {
28    let pack_dir = args
29        .input
30        .canonicalize()
31        .with_context(|| format!("failed to resolve pack dir {}", args.input.display()))?;
32    let lock_path = resolve_lock_path(&pack_dir, args.lock.as_deref());
33
34    let config = load_pack_config(&pack_dir)?;
35    let mut entries: Vec<LockedComponent> = Vec::new();
36    let _ = runtime;
37    for flow in &config.flows {
38        let summary = read_flow_resolve_summary_for_flow(&pack_dir, flow)?;
39        collect_from_summary(&pack_dir, flow, &summary, &mut entries)?;
40    }
41
42    let lock = PackLockV1::new(entries);
43    write_pack_lock(&lock_path, &lock)?;
44    if emit_path {
45        eprintln!("wrote {}", lock_path.display());
46    }
47
48    Ok(())
49}
50
51fn resolve_lock_path(pack_dir: &Path, override_path: Option<&Path>) -> PathBuf {
52    match override_path {
53        Some(path) if path.is_absolute() => path.to_path_buf(),
54        Some(path) => pack_dir.join(path),
55        None => pack_dir.join("pack.lock.json"),
56    }
57}
58
59fn collect_from_summary(
60    pack_dir: &Path,
61    flow: &crate::config::FlowConfig,
62    doc: &FlowResolveSummaryV1,
63    out: &mut Vec<LockedComponent>,
64) -> Result<()> {
65    let mut seen: BTreeMap<String, (String, String, ComponentId)> = BTreeMap::new();
66
67    for (node, resolve) in &doc.nodes {
68        let name = format!("{}___{node}", flow.id);
69        let source_ref = &resolve.source;
70        let (reference, digest) = match source_ref {
71            FlowResolveSummarySourceRefV1::Local { path } => {
72                let abs = normalize_local(pack_dir, flow, path)?;
73                (
74                    format!("file://{}", abs.to_string_lossy()),
75                    resolve.digest.clone(),
76                )
77            }
78            FlowResolveSummarySourceRefV1::Oci { .. }
79            | FlowResolveSummarySourceRefV1::Repo { .. }
80            | FlowResolveSummarySourceRefV1::Store { .. } => {
81                (format_reference(source_ref), resolve.digest.clone())
82            }
83        };
84        seen.insert(name, (reference, digest, resolve.component_id.clone()));
85    }
86
87    for (name, (reference, digest, component_id)) in seen {
88        out.push(LockedComponent {
89            name,
90            r#ref: reference,
91            digest,
92            component_id: Some(component_id),
93        });
94    }
95
96    Ok(())
97}
98
99fn normalize_local(
100    pack_dir: &Path,
101    flow: &crate::config::FlowConfig,
102    rel: &str,
103) -> Result<PathBuf> {
104    let flow_path = if flow.file.is_absolute() {
105        flow.file.clone()
106    } else {
107        pack_dir.join(&flow.file)
108    };
109    let parent = flow_path
110        .parent()
111        .ok_or_else(|| anyhow!("flow path {} has no parent", flow_path.display()))?;
112    let rel = rel.strip_prefix("file://").unwrap_or(rel);
113    Ok(parent.join(rel))
114}
115
116fn format_reference(source: &FlowResolveSummarySourceRefV1) -> String {
117    match source {
118        FlowResolveSummarySourceRefV1::Local { path } => path.clone(),
119        FlowResolveSummarySourceRefV1::Oci { r#ref } => {
120            if r#ref.contains("://") {
121                r#ref.clone()
122            } else {
123                format!("oci://{}", r#ref)
124            }
125        }
126        FlowResolveSummarySourceRefV1::Repo { r#ref } => {
127            if r#ref.contains("://") {
128                r#ref.clone()
129            } else {
130                format!("repo://{}", r#ref)
131            }
132        }
133        FlowResolveSummarySourceRefV1::Store { r#ref } => {
134            if r#ref.contains("://") {
135                r#ref.clone()
136            } else {
137                format!("store://{}", r#ref)
138            }
139        }
140    }
141}