Skip to main content

greentic_bundle/build/
plan.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5
6use super::manifest::{BundleManifest, ResolvedReferencePolicy, ResolvedTargetSummary};
7
8#[derive(Debug, Clone)]
9pub struct BuildState {
10    pub root: PathBuf,
11    pub build_dir: PathBuf,
12    pub manifest: BundleManifest,
13    pub lock: crate::project::BundleLock,
14    pub bundle_yaml: String,
15    pub resolved_files: Vec<(String, String)>,
16    pub setup_files: Vec<(String, String)>,
17    pub asset_files: Vec<(String, Vec<u8>)>,
18}
19
20pub fn build_state(root: &Path) -> Result<BuildState> {
21    let lock = crate::project::read_bundle_lock(root)
22        .with_context(|| format!("read {}", root.join(crate::project::LOCK_FILE).display()))?;
23    let bundle_yaml = fs::read_to_string(root.join(crate::project::WORKSPACE_ROOT_FILE))
24        .with_context(|| {
25            format!(
26                "read {}",
27                root.join(crate::project::WORKSPACE_ROOT_FILE).display()
28            )
29        })?;
30
31    let bundle_id =
32        find_yaml_scalar(&bundle_yaml, "bundle_id").unwrap_or_else(|| lock.bundle_id.clone());
33    let bundle_name =
34        find_yaml_scalar(&bundle_yaml, "bundle_name").unwrap_or_else(|| bundle_id.clone());
35    let requested_mode =
36        find_yaml_scalar(&bundle_yaml, "mode").unwrap_or_else(|| lock.requested_mode.clone());
37    let locale = find_yaml_scalar(&bundle_yaml, "locale").unwrap_or_else(|| "en".to_string());
38    let app_packs = find_yaml_list(&bundle_yaml, "app_packs");
39    let extension_providers = find_yaml_list(&bundle_yaml, "extension_providers");
40    let catalogs = find_yaml_list(&bundle_yaml, "remote_catalogs");
41    let hooks = find_yaml_list(&bundle_yaml, "hooks");
42    let subscriptions = find_yaml_list(&bundle_yaml, "subscriptions");
43    let capabilities = find_yaml_list(&bundle_yaml, "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 find_yaml_scalar(raw: &str, key: &str) -> Option<String> {
180    raw.lines().find_map(|line| {
181        let (left, right) = line.split_once(':')?;
182        (left.trim() == key).then(|| right.trim().to_string())
183    })
184}
185
186fn find_yaml_list(raw: &str, key: &str) -> Vec<String> {
187    let mut lines = raw.lines().peekable();
188    while let Some(line) = lines.next() {
189        let Some((left, right)) = line.split_once(':') else {
190            continue;
191        };
192        if left.trim() != key {
193            continue;
194        }
195        let inline = right.trim();
196        if inline == "[]" || inline == "[ ]" {
197            return Vec::new();
198        }
199        if !inline.is_empty() {
200            return vec![inline.to_string()];
201        }
202        let mut items = Vec::new();
203        while let Some(next) = lines.peek().copied() {
204            let trimmed = next.trim();
205            if let Some(value) = trimmed.strip_prefix("- ") {
206                items.push(value.trim().to_string());
207                lines.next();
208            } else {
209                break;
210            }
211        }
212        return items;
213    }
214    Vec::new()
215}
216
217fn parse_resolved_target(path: &str, raw: &str) -> Option<ResolvedTargetSummary> {
218    let tenant = find_yaml_scalar(raw, "tenant")?;
219    let default_policy =
220        find_yaml_scalar(raw, "default").unwrap_or_else(|| "forbidden".to_string());
221    let tenant_gmap = find_yaml_scalar(raw, "tenant_gmap")?;
222    let team_gmap = find_yaml_scalar(raw, "team_gmap");
223    let team = find_yaml_scalar(raw, "team");
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(raw),
232    })
233}
234
235fn parse_reference_policies(raw: &str) -> Vec<ResolvedReferencePolicy> {
236    let mut lines = raw.lines().peekable();
237    while let Some(line) = lines.next() {
238        let Some((left, _)) = line.split_once(':') else {
239            continue;
240        };
241        if left.trim() != "app_packs" {
242            continue;
243        }
244
245        let mut entries = Vec::new();
246        while let Some(next) = lines.peek().copied() {
247            let trimmed = next.trim();
248            if trimmed == "[]" {
249                lines.next();
250                break;
251            }
252            let Some(reference) = trimmed.strip_prefix("- reference:") else {
253                break;
254            };
255            let reference = reference.trim().to_string();
256            lines.next();
257
258            let mut policy = "unset".to_string();
259            if let Some(policy_line) = lines.peek().copied()
260                && let Some(value) = policy_line.trim().strip_prefix("policy:")
261            {
262                policy = value.trim().to_string();
263                lines.next();
264            }
265            entries.push(ResolvedReferencePolicy { reference, policy });
266        }
267        return entries;
268    }
269    Vec::new()
270}