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