greentic_component/cmd/
store.rs1use 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(StoreFetchArgs),
15}
16
17#[derive(Args, Debug, Clone)]
18pub struct StoreFetchArgs {
19 #[arg(long, value_name = "DIR")]
21 pub out: PathBuf,
22 #[arg(long, value_name = "DIR")]
24 pub cache_dir: Option<PathBuf>,
25 #[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}