Skip to main content

greentic_component/cmd/
store.rs

1use std::fs;
2use std::path::PathBuf;
3
4use anyhow::{Context, Result, anyhow};
5use clap::{Args, Subcommand};
6use serde_json::Value;
7
8use crate::path_safety::normalize_under_root;
9use greentic_distributor_client::{DistClient, DistOptions};
10
11#[derive(Subcommand, Debug, Clone)]
12pub enum StoreCommand {
13    /// Fetch a component from a source and write the wasm bytes to disk
14    Fetch(StoreFetchArgs),
15}
16
17#[derive(Args, Debug, Clone)]
18pub struct StoreFetchArgs {
19    /// Destination directory for the fetched component bytes
20    #[arg(long, value_name = "DIR")]
21    pub out: PathBuf,
22    /// Optional cache directory for fetched components
23    #[arg(long, value_name = "DIR")]
24    pub cache_dir: Option<PathBuf>,
25    /// Source reference to resolve (file://, oci://, repo://, store://, etc.)
26    #[arg(value_name = "SOURCE")]
27    pub source: String,
28}
29
30pub fn run(command: StoreCommand) -> Result<()> {
31    match command {
32        StoreCommand::Fetch(args) => fetch(args),
33    }
34}
35
36fn fetch(args: StoreFetchArgs) -> Result<()> {
37    let mut opts = DistOptions::default();
38    if let Some(cache_dir) = &args.cache_dir {
39        opts.cache_dir = cache_dir.clone();
40    }
41    let client = DistClient::new(opts);
42    let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
43    let resolved = rt
44        .block_on(async { client.ensure_cached(&args.source).await })
45        .context("store fetch failed")?;
46    let cache_path = resolved
47        .cache_path
48        .ok_or_else(|| anyhow!("resolved source has no cached component path"))?;
49    let (out_dir, wasm_override) = resolve_output_paths(&args.out)?;
50    fs::create_dir_all(&out_dir)
51        .with_context(|| format!("failed to create output dir {}", out_dir.display()))?;
52    let manifest_cache_path = cache_path
53        .parent()
54        .map(|dir| dir.join("component.manifest.json"));
55    let manifest_out_path = out_dir.join("component.manifest.json");
56    let mut wasm_out_path = wasm_override
57        .clone()
58        .unwrap_or_else(|| out_dir.join("component.wasm"));
59    if let Some(manifest_cache_path) = manifest_cache_path
60        && manifest_cache_path.exists()
61    {
62        let manifest_bytes = fs::read(&manifest_cache_path).with_context(|| {
63            format!(
64                "failed to read cached manifest {}",
65                manifest_cache_path.display()
66            )
67        })?;
68        fs::write(&manifest_out_path, &manifest_bytes)
69            .with_context(|| format!("failed to write manifest {}", manifest_out_path.display()))?;
70        let manifest: Value = serde_json::from_slice(&manifest_bytes).with_context(|| {
71            format!(
72                "failed to parse component.manifest.json from {}",
73                manifest_cache_path.display()
74            )
75        })?;
76        if let Some(component_wasm) = manifest
77            .get("artifacts")
78            .and_then(|artifacts| artifacts.get("component_wasm"))
79            .and_then(|value| value.as_str())
80        {
81            let candidate = PathBuf::from(component_wasm);
82            if wasm_override.is_none() {
83                wasm_out_path = normalize_under_root(&out_dir, &candidate).with_context(|| {
84                    format!("invalid artifacts.component_wasm path `{}`", component_wasm)
85                })?;
86                if let Some(parent) = wasm_out_path.parent() {
87                    fs::create_dir_all(parent).with_context(|| {
88                        format!("failed to create output dir {}", parent.display())
89                    })?;
90                }
91            }
92        }
93    }
94    fs::copy(&cache_path, &wasm_out_path).with_context(|| {
95        format!(
96            "failed to copy cached component {} to {}",
97            cache_path.display(),
98            wasm_out_path.display()
99        )
100    })?;
101    println!(
102        "Wrote {} (digest {}) for source {}",
103        wasm_out_path.display(),
104        resolved.digest,
105        args.source,
106    );
107    if manifest_out_path.exists() {
108        println!("Wrote {}", manifest_out_path.display());
109    }
110    Ok(())
111}
112
113fn resolve_output_paths(out: &std::path::Path) -> Result<(PathBuf, Option<PathBuf>)> {
114    if out.exists() {
115        if out.is_dir() {
116            return Ok((out.to_path_buf(), None));
117        }
118        if let Some(parent) = out.parent() {
119            return Ok((parent.to_path_buf(), Some(out.to_path_buf())));
120        }
121        return Ok((PathBuf::from("."), Some(out.to_path_buf())));
122    }
123
124    if out.extension().is_some() {
125        let parent = out.parent().unwrap_or_else(|| std::path::Path::new("."));
126        return Ok((parent.to_path_buf(), Some(out.to_path_buf())));
127    }
128
129    Ok((out.to_path_buf(), None))
130}