1use anyhow::{Result, anyhow};
2use colored::*;
3use ignore::gitignore::GitignoreBuilder;
4use mlua::{Lua, LuaSerdeExt, Table, Value};
5use std::collections::HashSet;
6use std::fs::{self, File};
7use std::path::{Path, PathBuf};
8use tar::Builder as TarBuilder;
9use tempfile::Builder;
10use walkdir::WalkDir;
11use zstd::stream::write::Encoder as ZstdEncoder;
12
13pub fn run(
14 package_file: &Path,
15 output_dir: Option<&Path>,
16 sign: Option<String>,
17 version_override: Option<&str>,
18 build_type: Option<String>,
19) -> Result<()> {
20 let pkg_dir = package_file
21 .parent()
22 .ok_or_else(|| anyhow!("Could not get parent directory"))?;
23
24 let mut ignore_builder = GitignoreBuilder::new(pkg_dir);
26 let zoiignore_path = pkg_dir.join(".zoiignore");
27 if zoiignore_path.exists()
28 && let Some(err) = ignore_builder.add(&zoiignore_path)
29 {
30 eprintln!("{}: Error parsing .zoiignore: {}", "Warning".yellow(), err);
31 }
32 let ignore = ignore_builder.build()?;
33
34 let is_ignored =
35 |rel_path: &Path, is_dir: bool| -> bool { ignore.matched(rel_path, is_dir).is_ignore() };
36
37 println!(
38 "{} Bundling package: {}",
39 "::".bold().blue(),
40 package_file.display()
41 );
42
43 let lua = Lua::new();
44 let platform = zoi_core::utils::get_platform()?;
45
46 let refs_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
48 lua.globals()
49 .set("__ZoiReferencedFiles", refs_table)
50 .map_err(|e| anyhow!(e.to_string()))?;
51
52 let bundle_type = build_type.as_deref().unwrap_or("source");
56
57 zoi_lua::functions::setup_lua_environment(
59 &lua,
60 &platform,
61 version_override,
62 package_file.to_str(),
63 None,
64 Some("/tmp/mock-build"),
65 Some("/tmp/mock-staging"),
66 None,
67 None,
68 Some(bundle_type),
69 true, )
71 .map_err(|e| anyhow!(e.to_string()))?;
72
73 lua.globals()
74 .set("BUILD_TYPE", bundle_type)
75 .map_err(|e| anyhow!(e.to_string()))?;
76
77 if let Ok(utils) = lua.globals().get::<Table>("UTILS") {
79 let mock_extract = lua
80 .create_function(|_, (_source, _out_dir): (String, String)| Ok(()))
81 .map_err(|e| anyhow!(e.to_string()))?;
82 utils
83 .set("EXTRACT", mock_extract)
84 .map_err(|e| anyhow!(e.to_string()))?;
85 }
86
87 let mock_cmd = lua
89 .create_function(|_, _command: String| Ok((String::new(), String::new(), 0)))
90 .map_err(|e| anyhow!(e.to_string()))?;
91 lua.globals()
92 .set("cmd", mock_cmd)
93 .map_err(|e| anyhow!(e.to_string()))?;
94
95 let lua_code = fs::read_to_string(package_file)?;
97 lua.load(&lua_code).exec().map_err(|e| {
98 anyhow!(
99 "Failed to execute Lua package file '{}' for bundling:\n{}",
100 package_file.display(),
101 e
102 )
103 })?;
104
105 let args = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
106
107 if let Ok(pkg_fn) = lua.globals().get::<mlua::Function>("package") {
109 let _ = pkg_fn.call::<()>(args.clone());
110 }
111
112 let mut files_to_include = HashSet::new();
113
114 let pkg_filename = package_file
116 .file_name()
117 .ok_or_else(|| anyhow!("Invalid package file"))?;
118 files_to_include.insert(pkg_filename.to_string_lossy().to_string());
119
120 if let Ok(refs) = lua.globals().get::<Table>("__ZoiReferencedFiles") {
122 for val in refs.sequence_values::<String>() {
123 files_to_include.insert(val.map_err(|e| anyhow!(e.to_string()))?);
124 }
125 }
126
127 if let Ok(ops) = lua.globals().get::<Table>("__ZoiBuildOperations") {
129 for op in ops.sequence_values::<Table>() {
130 let op = op.map_err(|e| anyhow!(e.to_string()))?;
131
132 if let Ok(source) = op.get::<String>("source")
134 && let Some(rel) = source.strip_prefix("${pkgluadir}/")
135 {
136 files_to_include.insert(rel.to_string());
137 }
138
139 if let Ok(target) = op.get::<String>("target")
141 && let Some(rel) = target.strip_prefix("${pkgluadir}/")
142 {
143 files_to_include.insert(rel.to_string());
144 }
145 }
146 }
147
148 println!("{} Fetching upstream sources...", "::".bold().blue());
150 let fetch_dir = Builder::new().prefix("zoi-bundle-fetch-").tempdir()?;
151
152 let lua_fetch = Lua::new();
154
155 let pkg_meta_table_f = lua_fetch
157 .create_table()
158 .map_err(|e| anyhow!(e.to_string()))?;
159 let pkg_deps_table_f = lua_fetch
160 .create_table()
161 .map_err(|e| anyhow!(e.to_string()))?;
162 let pkg_updates_table_f = lua_fetch
163 .create_table()
164 .map_err(|e| anyhow!(e.to_string()))?;
165 let pkg_hooks_table_f = lua_fetch
166 .create_table()
167 .map_err(|e| anyhow!(e.to_string()))?;
168 let pkg_service_table_f = lua_fetch
169 .create_table()
170 .map_err(|e| anyhow!(e.to_string()))?;
171 lua_fetch
172 .globals()
173 .set("__ZoiPackageMeta", pkg_meta_table_f)
174 .map_err(|e| anyhow!(e.to_string()))?;
175 lua_fetch
176 .globals()
177 .set("__ZoiPackageDeps", pkg_deps_table_f)
178 .map_err(|e| anyhow!(e.to_string()))?;
179 lua_fetch
180 .globals()
181 .set("__ZoiPackageUpdates", pkg_updates_table_f)
182 .map_err(|e| anyhow!(e.to_string()))?;
183 lua_fetch
184 .globals()
185 .set("__ZoiPackageHooks", pkg_hooks_table_f)
186 .map_err(|e| anyhow!(e.to_string()))?;
187 lua_fetch
188 .globals()
189 .set("__ZoiPackageService", pkg_service_table_f)
190 .map_err(|e| anyhow!(e.to_string()))?;
191
192 let pkg_global_table_f = lua_fetch
193 .create_table()
194 .map_err(|e| anyhow!(e.to_string()))?;
195 lua_fetch
196 .globals()
197 .set("PKG", pkg_global_table_f)
198 .map_err(|e| anyhow!(e.to_string()))?;
199
200 zoi_lua::functions::setup_lua_environment(
201 &lua_fetch,
202 &platform,
203 version_override,
204 package_file.to_str(),
205 None,
206 Some(fetch_dir.path().to_str().unwrap_or("")),
207 Some("/tmp/mock-staging"),
208 None,
209 None,
210 Some(bundle_type),
211 true, )
213 .map_err(|e| anyhow!(e.to_string()))?;
214
215 lua_fetch
216 .globals()
217 .set("BUILD_TYPE", bundle_type)
218 .map_err(|e| anyhow!(e.to_string()))?;
219
220 lua_fetch
221 .globals()
222 .set(
223 "BUILD_DIR",
224 fetch_dir
225 .path()
226 .to_str()
227 .ok_or_else(|| anyhow!("Invalid fetch path"))?,
228 )
229 .map_err(|e| anyhow!(e.to_string()))?;
230
231 zoi_lua::api::system::add_cmd_util(&lua_fetch, true).map_err(|e| anyhow!(e.to_string()))?;
233
234 lua_fetch.load(&lua_code).exec().map_err(|e| {
236 anyhow!(
237 "Failed to execute Lua package file '{}' during fetch:\n{}",
238 package_file.display(),
239 e
240 )
241 })?;
242
243 if let Ok(prep_fn) = lua_fetch.globals().get::<mlua::Function>("prepare") {
244 println!(" Running prepare()...");
245 let args_fetch = lua_fetch
246 .create_table()
247 .map_err(|e| anyhow!(e.to_string()))?;
248 prep_fn.call::<()>(args_fetch).map_err(|e| {
249 anyhow!(
250 "The 'prepare' function in '{}' failed during bundling:\n{}",
251 package_file.display(),
252 e
253 )
254 })?;
255 }
256
257 let pkg_dir = package_file
259 .parent()
260 .ok_or_else(|| anyhow!("Could not get parent directory"))?;
261
262 let final_pkg_meta: Table = lua
263 .globals()
264 .get("__ZoiPackageMeta")
265 .map_err(|e| anyhow!(e.to_string()))?;
266 let pkg_meta: zoi_core::types::Package = lua
267 .from_value(Value::Table(final_pkg_meta))
268 .map_err(|e| anyhow!(e.to_string()))?;
269
270 let version = version_override
271 .map(|v| v.to_string())
272 .or(pkg_meta.version)
273 .unwrap_or_else(|| "unknown".to_string());
274 let output_filename = format!("{}-{}.zsa", pkg_meta.name, version);
275 let output_base = output_dir
276 .map(|d| d.to_path_buf())
277 .unwrap_or_else(|| pkg_dir.to_path_buf());
278 let output_path = output_base.join(output_filename);
279
280 let file = File::create(&output_path)?;
281 let encoder = ZstdEncoder::new(file, 0)?.auto_finish();
282 let mut tar_builder = TarBuilder::new(encoder);
283
284 let mut sorted_files: Vec<_> = files_to_include.into_iter().collect();
286 sorted_files.sort();
287
288 for rel_path_str in sorted_files {
289 let rel_path = Path::new(&rel_path_str);
290 let abs_path = pkg_dir.join(rel_path);
291 let is_dir = abs_path.is_dir();
292
293 if is_ignored(rel_path, is_dir) {
294 println!(" Ignored: {}", rel_path_str);
295 continue;
296 }
297
298 if abs_path.exists() {
299 if is_dir {
300 let mut it = WalkDir::new(&abs_path).into_iter();
302 loop {
303 let entry = match it.next() {
304 None => break,
305 Some(Err(e)) => return Err(e.into()),
306 Some(Ok(e)) => e,
307 };
308 let entry_rel = entry.path().strip_prefix(pkg_dir)?;
309 let entry_is_dir = entry.file_type().is_dir();
310 if is_ignored(entry_rel, entry_is_dir) {
311 if entry_is_dir {
312 it.skip_current_dir();
313 }
314 continue;
315 }
316
317 if entry.file_type().is_file() {
318 tar_builder.append_path_with_name(entry.path(), entry_rel)?;
319 println!(" Included local: {}", entry_rel.display());
320 }
321 }
322 } else {
323 tar_builder.append_path_with_name(&abs_path, &rel_path_str)?;
324 println!(" Included local: {}", rel_path_str);
325 }
326 }
327 }
328
329 let mut it = WalkDir::new(fetch_dir.path()).into_iter();
331 loop {
332 let entry = match it.next() {
333 None => break,
334 Some(Err(e)) => return Err(e.into()),
335 Some(Ok(e)) => e,
336 };
337
338 if entry.depth() == 0 {
339 continue;
340 }
341
342 let rel_path = entry.path().strip_prefix(fetch_dir.path())?;
343 let rel_path_str = rel_path.to_string_lossy();
344 let is_dir = entry.file_type().is_dir();
345
346 if is_ignored(rel_path, is_dir) {
347 if is_dir {
348 it.skip_current_dir();
349 }
350 println!(" Ignored fetch: {}", rel_path_str);
351 continue;
352 }
353
354 if entry.file_type().is_dir() {
355 continue;
357 }
358
359 tar_builder.append_path_with_name(entry.path(), rel_path)?;
360 println!(" Included fetch: {}", rel_path_str);
361 }
362
363 let mut header = tar::Header::new_gnu();
365 header.set_path(".zoi-prepared")?;
366 header.set_size(0);
367 header.set_cksum();
368 tar_builder.append(&header, &[][..])?;
369
370 tar_builder.finish()?;
371 println!(
372 "{} Successfully created bundle: {}",
373 "::".bold().green(),
374 output_path.display()
375 );
376
377 if let Some(key_id) = sign {
378 println!(
379 "{} Signing bundle with key '{}'...",
380 "::".bold().blue(),
381 key_id.cyan()
382 );
383 let signature_path = PathBuf::from(format!("{}.sig", output_path.display()));
384 if signature_path.exists() {
385 fs::remove_file(&signature_path)?;
386 }
387 zoi_core::pgp::sign_detached(&output_path, &signature_path, &key_id)?;
388 println!(
389 "{} Successfully created signature: {}",
390 "::".bold().green(),
391 signature_path.display()
392 );
393 }
394
395 Ok(())
396}