Skip to main content

greentic_bundle/build/
plan.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5use serde_yaml_bw::Value;
6
7use super::manifest::{BundleManifest, ResolvedReferencePolicy, ResolvedTargetSummary};
8
9#[derive(Debug, Clone)]
10pub struct BuildState {
11    pub root: PathBuf,
12    pub build_dir: PathBuf,
13    pub manifest: BundleManifest,
14    pub lock: crate::project::BundleLock,
15    pub bundle_yaml: String,
16    pub resolved_files: Vec<(String, String)>,
17    pub setup_files: Vec<(String, String)>,
18    pub asset_files: Vec<(String, Vec<u8>)>,
19}
20
21pub fn build_state(root: &Path) -> Result<BuildState> {
22    let lock = crate::project::read_bundle_lock(root)
23        .with_context(|| format!("read {}", root.join(crate::project::LOCK_FILE).display()))?;
24    let bundle_yaml = fs::read_to_string(root.join(crate::project::WORKSPACE_ROOT_FILE))
25        .with_context(|| {
26            format!(
27                "read {}",
28                root.join(crate::project::WORKSPACE_ROOT_FILE).display()
29            )
30        })?;
31
32    let bundle_doc = parse_yaml_document(&bundle_yaml);
33    let bundle_id = yaml_string(&bundle_doc, "bundle_id").unwrap_or_else(|| lock.bundle_id.clone());
34    let bundle_name = yaml_string(&bundle_doc, "bundle_name").unwrap_or_else(|| bundle_id.clone());
35    let requested_mode =
36        yaml_string(&bundle_doc, "mode").unwrap_or_else(|| lock.requested_mode.clone());
37    let locale = yaml_string(&bundle_doc, "locale").unwrap_or_else(|| "en".to_string());
38    let app_packs = yaml_string_list(&bundle_doc, "app_packs");
39    let extension_providers = yaml_string_list(&bundle_doc, "extension_providers");
40    let catalogs = yaml_string_list(&bundle_doc, "remote_catalogs");
41    let hooks = yaml_string_list(&bundle_doc, "hooks");
42    let subscriptions = yaml_string_list(&bundle_doc, "subscriptions");
43    let capabilities = yaml_string_list(&bundle_doc, "capabilities");
44    let resolved_files = collect_files(root, &root.join("resolved"))?;
45    let setup_files = collect_named_files(root, &lock.setup_state_files)?;
46    let asset_files = collect_asset_files(root)?;
47    let resolved_targets = resolved_files
48        .iter()
49        .filter_map(|(name, contents)| parse_resolved_target(name, contents))
50        .collect();
51
52    let manifest = BundleManifest {
53        format_version: crate::build::BUILD_FORMAT_VERSION.to_string(),
54        bundle_id,
55        bundle_name,
56        requested_mode,
57        locale,
58        artifact_extension: crate::build::FUTURE_ARTIFACT_EXTENSION.to_string(),
59        generated_resolved_files: resolved_files
60            .iter()
61            .map(|(name, _)| name.clone())
62            .collect(),
63        generated_setup_files: setup_files.iter().map(|(name, _)| name.clone()).collect(),
64        app_packs,
65        extension_providers,
66        catalogs,
67        hooks,
68        subscriptions,
69        capabilities,
70        resolved_targets,
71    };
72
73    let build_dir = root
74        .join(crate::build::BUILD_STATE_DIR)
75        .join(&manifest.bundle_id)
76        .join("normalized");
77    Ok(BuildState {
78        root: root.to_path_buf(),
79        build_dir,
80        manifest,
81        lock,
82        bundle_yaml,
83        resolved_files,
84        setup_files,
85        asset_files,
86    })
87}
88
89pub fn load_build_state(build_dir: &Path) -> Result<BuildState> {
90    let manifest_raw = fs::read_to_string(build_dir.join("bundle-manifest.json"))?;
91    let lock_raw = fs::read_to_string(build_dir.join("bundle-lock.json"))?;
92    let bundle_yaml = fs::read_to_string(build_dir.join("bundle.yaml"))?;
93    let manifest = serde_json::from_str::<BundleManifest>(&manifest_raw)?;
94    let lock = serde_json::from_str::<crate::project::BundleLock>(&lock_raw)?;
95    let resolved_files = collect_files(build_dir, &build_dir.join("resolved"))?;
96    let setup_files = collect_files(build_dir, &build_dir.join("state").join("setup"))?;
97    let asset_files = collect_asset_files(build_dir)?;
98    Ok(BuildState {
99        root: build_dir.to_path_buf(),
100        build_dir: build_dir.to_path_buf(),
101        manifest,
102        lock,
103        bundle_yaml,
104        resolved_files,
105        setup_files,
106        asset_files,
107    })
108}
109
110fn collect_asset_files(root: &Path) -> Result<Vec<(String, Vec<u8>)>> {
111    let mut files = Vec::new();
112    for relative_root in ["packs", "providers", "tenants"] {
113        let dir = root.join(relative_root);
114        if !dir.exists() {
115            continue;
116        }
117        for entry in walk(&dir)? {
118            if entry.extension().and_then(|value| value.to_str()) != Some("gtpack") {
119                continue;
120            }
121            let rel = entry
122                .strip_prefix(root)
123                .unwrap_or(&entry)
124                .display()
125                .to_string();
126            files.push((rel, fs::read(&entry)?));
127        }
128    }
129    files.sort_by(|a, b| a.0.cmp(&b.0));
130    files.dedup_by(|left, right| left.0 == right.0);
131    Ok(files)
132}
133
134fn collect_files(root: &Path, dir: &Path) -> Result<Vec<(String, String)>> {
135    if !dir.exists() {
136        return Ok(Vec::new());
137    }
138    let mut files = Vec::new();
139    for entry in walk(dir)? {
140        let rel = entry
141            .strip_prefix(root)
142            .unwrap_or(&entry)
143            .display()
144            .to_string();
145        let contents = fs::read_to_string(&entry)?;
146        files.push((rel, contents));
147    }
148    files.sort_by(|a, b| a.0.cmp(&b.0));
149    Ok(files)
150}
151
152fn collect_named_files(root: &Path, names: &[String]) -> Result<Vec<(String, String)>> {
153    let mut files = Vec::new();
154    for name in names {
155        let path = root.join(name);
156        if path.exists() {
157            files.push((name.clone(), fs::read_to_string(path)?));
158        }
159    }
160    files.sort_by(|a, b| a.0.cmp(&b.0));
161    Ok(files)
162}
163
164fn walk(dir: &Path) -> Result<Vec<PathBuf>> {
165    let mut out = Vec::new();
166    for entry in fs::read_dir(dir)? {
167        let entry = entry?;
168        let path = entry.path();
169        if entry.file_type()?.is_dir() {
170            out.extend(walk(&path)?);
171        } else {
172            out.push(path);
173        }
174    }
175    out.sort();
176    Ok(out)
177}
178
179fn parse_yaml_document(raw: &str) -> Option<Value> {
180    serde_yaml_bw::from_str(raw).ok()
181}
182
183fn yaml_string(doc: &Option<Value>, key: &str) -> Option<String> {
184    doc.as_ref()?.get(key)?.as_str().map(ToOwned::to_owned)
185}
186
187fn yaml_string_list(doc: &Option<Value>, key: &str) -> Vec<String> {
188    match doc.as_ref().and_then(|value| value.get(key)) {
189        Some(Value::Sequence(items)) => items
190            .iter()
191            .filter_map(|item| item.as_str().map(ToOwned::to_owned))
192            .collect(),
193        Some(Value::Null(_)) | None => Vec::new(),
194        Some(Value::String(value, _)) => vec![value.clone()],
195        _ => Vec::new(),
196    }
197}
198
199fn parse_resolved_target(path: &str, raw: &str) -> Option<ResolvedTargetSummary> {
200    let doc = parse_yaml_document(raw)?;
201    let tenant = doc.get("tenant")?.as_str()?.to_string();
202    let default_policy = doc
203        .get("policy")
204        .and_then(|value| value.get("default"))
205        .and_then(Value::as_str)
206        .unwrap_or("forbidden")
207        .to_string();
208    let tenant_gmap = doc
209        .get("policy")
210        .and_then(|value| value.get("source"))
211        .and_then(|value| value.get("tenant_gmap"))
212        .and_then(Value::as_str)?
213        .to_string();
214    let team_gmap = doc
215        .get("policy")
216        .and_then(|value| value.get("source"))
217        .and_then(|value| value.get("team_gmap"))
218        .and_then(Value::as_str)
219        .map(ToOwned::to_owned);
220    let team = doc
221        .get("team")
222        .and_then(Value::as_str)
223        .map(ToOwned::to_owned);
224    Some(ResolvedTargetSummary {
225        path: path.to_string(),
226        tenant,
227        team,
228        default_policy,
229        tenant_gmap,
230        team_gmap,
231        app_pack_policies: parse_reference_policies(&doc),
232    })
233}
234
235fn parse_reference_policies(doc: &Value) -> Vec<ResolvedReferencePolicy> {
236    let Some(Value::Sequence(items)) = doc.get("app_packs") else {
237        return Vec::new();
238    };
239
240    items
241        .iter()
242        .filter_map(|item| {
243            let reference = item.get("reference")?.as_str()?.to_string();
244            let policy = item
245                .get("policy")
246                .and_then(Value::as_str)
247                .unwrap_or("unset")
248                .to_string();
249            Some(ResolvedReferencePolicy { reference, policy })
250        })
251        .collect()
252}