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}