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"] {
116 let dir = root.join(relative_root);
117 if !dir.exists() {
118 continue;
119 }
120 for entry in walk(&dir)? {
121 if entry.extension().and_then(|value| value.to_str()) != Some("gtpack") {
122 continue;
123 }
124 let rel = entry
125 .strip_prefix(root)
126 .unwrap_or(&entry)
127 .display()
128 .to_string();
129 files.push((rel, fs::read(&entry)?));
130 }
131 }
132 for relative_root in [".providers", "assets", "tenants"] {
139 let dir = root.join(relative_root);
140 if !dir.exists() {
141 continue;
142 }
143 for entry in walk(&dir)? {
144 let rel = entry
145 .strip_prefix(root)
146 .unwrap_or(&entry)
147 .display()
148 .to_string();
149 files.push((rel, fs::read(&entry)?));
150 }
151 }
152 files.sort_by(|a, b| a.0.cmp(&b.0));
153 files.dedup_by(|left, right| left.0 == right.0);
154 Ok(files)
155}
156
157fn collect_files(root: &Path, dir: &Path) -> Result<Vec<(String, String)>> {
158 if !dir.exists() {
159 return Ok(Vec::new());
160 }
161 let mut files = Vec::new();
162 for entry in walk(dir)? {
163 let rel = entry
164 .strip_prefix(root)
165 .unwrap_or(&entry)
166 .display()
167 .to_string();
168 let contents = fs::read_to_string(&entry)?;
169 files.push((rel, contents));
170 }
171 files.sort_by(|a, b| a.0.cmp(&b.0));
172 Ok(files)
173}
174
175fn collect_named_files(root: &Path, names: &[String]) -> Result<Vec<(String, String)>> {
176 let mut files = Vec::new();
177 for name in names {
178 let path = root.join(name);
179 if path.exists() {
180 files.push((name.clone(), fs::read_to_string(path)?));
181 }
182 }
183 files.sort_by(|a, b| a.0.cmp(&b.0));
184 Ok(files)
185}
186
187fn walk(dir: &Path) -> Result<Vec<PathBuf>> {
188 let mut out = Vec::new();
189 for entry in fs::read_dir(dir)? {
190 let entry = entry?;
191 let path = entry.path();
192 if entry.file_type()?.is_dir() {
193 out.extend(walk(&path)?);
194 } else {
195 out.push(path);
196 }
197 }
198 out.sort();
199 Ok(out)
200}
201
202fn parse_yaml_document(raw: &str) -> Option<Value> {
203 serde_yaml_bw::from_str(raw).ok()
204}
205
206fn yaml_string(doc: &Option<Value>, key: &str) -> Option<String> {
207 doc.as_ref()?.get(key)?.as_str().map(ToOwned::to_owned)
208}
209
210fn yaml_string_list(doc: &Option<Value>, key: &str) -> Vec<String> {
211 match doc.as_ref().and_then(|value| value.get(key)) {
212 Some(Value::Sequence(items)) => items
213 .iter()
214 .filter_map(|item| item.as_str().map(ToOwned::to_owned))
215 .collect(),
216 Some(Value::Null(_)) | None => Vec::new(),
217 Some(Value::String(value, _)) => vec![value.clone()],
218 _ => Vec::new(),
219 }
220}
221
222fn parse_resolved_target(path: &str, raw: &str) -> Option<ResolvedTargetSummary> {
223 let doc = parse_yaml_document(raw)?;
224 let tenant = doc.get("tenant")?.as_str()?.to_string();
225 let default_policy = doc
226 .get("policy")
227 .and_then(|value| value.get("default"))
228 .and_then(Value::as_str)
229 .unwrap_or("forbidden")
230 .to_string();
231 let tenant_gmap = doc
232 .get("policy")
233 .and_then(|value| value.get("source"))
234 .and_then(|value| value.get("tenant_gmap"))
235 .and_then(Value::as_str)?
236 .to_string();
237 let team_gmap = doc
238 .get("policy")
239 .and_then(|value| value.get("source"))
240 .and_then(|value| value.get("team_gmap"))
241 .and_then(Value::as_str)
242 .map(ToOwned::to_owned);
243 let team = doc
244 .get("team")
245 .and_then(Value::as_str)
246 .map(ToOwned::to_owned);
247 Some(ResolvedTargetSummary {
248 path: path.to_string(),
249 tenant,
250 team,
251 default_policy,
252 tenant_gmap,
253 team_gmap,
254 app_pack_policies: parse_reference_policies(&doc),
255 })
256}
257
258fn parse_reference_policies(doc: &Value) -> Vec<ResolvedReferencePolicy> {
259 let Some(Value::Sequence(items)) = doc.get("app_packs") else {
260 return Vec::new();
261 };
262
263 items
264 .iter()
265 .filter_map(|item| {
266 let reference = item.get("reference")?.as_str()?.to_string();
267 let policy = item
268 .get("policy")
269 .and_then(Value::as_str)
270 .unwrap_or("unset")
271 .to_string();
272 Some(ResolvedReferencePolicy { reference, policy })
273 })
274 .collect()
275}