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}