1use std::fs;
7use std::path::Path;
8
9use anyhow::{Result, anyhow};
10use mlua::{self, Lua, LuaSerdeExt, Table, Value};
11use tar::Archive;
12use walkdir::WalkDir;
13use zoi_core::{types, utils};
14use zstd::stream::read::Decoder as ZstdDecoder;
15
16use crate::functions;
17
18pub fn parse_lua_package_from_archive(
26 archive_path: &Path,
27 version_override: Option<&str>,
28 scope: Option<types::Scope>,
29 quiet: bool
30) -> Result<types::Package> {
31 let platform = utils::get_platform()?;
32 parse_lua_package_from_archive_for_platform(
33 archive_path,
34 &platform,
35 version_override,
36 scope,
37 quiet
38 )
39}
40
41pub fn parse_lua_package_from_archive_for_platform(
47 archive_path: &Path,
48 platform: &str,
49 version_override: Option<&str>,
50 scope: Option<types::Scope>,
51 quiet: bool
52) -> Result<types::Package> {
53 let file = fs::File::open(archive_path)?;
54 let decoder = ZstdDecoder::new(file)?;
55 let mut archive = Archive::new(decoder);
56 let temp_dir = tempfile::Builder::new()
57 .prefix("zoi-arch-parse-")
58 .tempdir()?;
59 archive.unpack(temp_dir.path())?;
60
61 let mut pkg_lua = None;
62 for entry in WalkDir::new(temp_dir.path())
63 .into_iter()
64 .filter_map(Result::ok)
65 {
66 if entry.file_name().to_string_lossy().ends_with(".pkg.lua") {
67 pkg_lua = Some(entry.path().to_path_buf());
68 break;
69 }
70 }
71
72 let pkg_lua_path =
73 pkg_lua.ok_or_else(|| anyhow!("No .pkg.lua in archive"))?;
74 parse_lua_package_from_file_for_platform(
75 pkg_lua_path
76 .to_str()
77 .ok_or_else(|| anyhow!("Invalid path"))?,
78 platform,
79 version_override,
80 scope,
81 quiet
82 )
83}
84
85pub fn parse_lua_package_for_platform(
91 file_path: &str,
92 platform: &str,
93 version_override: Option<&str>,
94 scope: Option<types::Scope>,
95 quiet: bool
96) -> Result<types::Package> {
97 let path = Path::new(file_path);
98 if path.extension().is_some_and(|ext| {
99 ext.eq_ignore_ascii_case("zpa") || ext.eq_ignore_ascii_case("zsa")
100 }) {
101 return parse_lua_package_from_archive_for_platform(
102 path,
103 platform,
104 version_override,
105 scope,
106 quiet
107 );
108 }
109 parse_lua_package_from_file_for_platform(
110 file_path,
111 platform,
112 version_override,
113 scope,
114 quiet
115 )
116}
117
118fn parse_lua_package_from_file_for_platform(
124 file_path: &str,
125 platform: &str,
126 version_override: Option<&str>,
127 scope: Option<types::Scope>,
128 quiet: bool
129) -> Result<types::Package> {
130 let lua_code = fs::read_to_string(file_path)?;
131 let lua = Lua::new();
132
133 functions::setup_lua_environment(
134 &lua,
135 platform,
136 version_override,
137 Some(file_path),
138 None,
139 None,
140 None,
141 None,
142 scope,
143 None,
144 quiet
145 )
146 .map_err(|e| {
147 anyhow!("Failed to setup Lua environment for '{file_path}': {e}")
148 })?;
149
150 lua.load(&lua_code).exec().map_err(|e| {
151 use colored::Colorize;
152 let error_msg = format!("{e}");
153 let enriched_msg = if let Some(line) = extract_line_number(&error_msg) {
154 let lines: Vec<&str> = lua_code.lines().collect();
155 if let Some(code_line) =
156 lines.get((line as usize).saturating_sub(1))
157 {
158 format!(
159 "{}\n\n{} | {}\n | ^",
160 error_msg.red().bold(),
161 format!("{line:4}").dimmed(),
162 code_line.cyan()
163 )
164 } else {
165 error_msg
166 }
167 } else {
168 error_msg
169 };
170 anyhow!(
171 "Failed to execute Lua package file '{file_path}':\n{enriched_msg}"
172 )
173 })?;
174
175 let final_pkg_meta: Table = lua
176 .globals()
177 .get("__ZoiPackageMeta")
178 .map_err(|e| anyhow!(e.to_string()))?;
179 let final_pkg_deps: Table = lua
180 .globals()
181 .get("__ZoiPackageDeps")
182 .map_err(|e| anyhow!(e.to_string()))?;
183 let final_pkg_updates: Table = lua
184 .globals()
185 .get("__ZoiPackageUpdates")
186 .map_err(|e| anyhow!(e.to_string()))?;
187 let final_pkg_hooks: Table = lua
188 .globals()
189 .get("__ZoiPackageHooks")
190 .map_err(|e| anyhow!(e.to_string()))?;
191 let final_pkg_service: Table = lua
192 .globals()
193 .get("__ZoiPackageService")
194 .map_err(|e| anyhow!(e.to_string()))?;
195
196 let mut package: types::Package = lua
197 .from_value(Value::Table(final_pkg_meta.clone()))
198 .map_err(|e| {
199 anyhow!(
200 "Failed to parse 'metadata' block in package file \
201 '{file_path}':\n{e}"
202 )
203 })?;
204
205 package.zoios = final_pkg_meta.get("zoios").ok();
208
209 package.dependencies = if final_pkg_deps.is_empty() {
210 None
211 } else {
212 Some(lua.from_value(Value::Table(final_pkg_deps)).map_err(|e| {
213 anyhow!(
214 "Failed to parse 'dependencies' block in package file \
215 '{file_path}':\n{e}"
216 )
217 })?)
218 };
219
220 package.updates = if final_pkg_updates.is_empty() {
221 None
222 } else {
223 Some(
224 lua.from_value(Value::Table(final_pkg_updates))
225 .map_err(|e| {
226 anyhow!(
227 "Failed to parse 'updates' block in package file \
228 '{file_path}':
229{e}"
230 )
231 })?
232 )
233 };
234
235 package.hooks = if final_pkg_hooks.is_empty() {
236 None
237 } else {
238 Some(lua.from_value(Value::Table(final_pkg_hooks)).map_err(|e| {
239 anyhow!(
240 "Failed to parse 'hooks' block in package file '{file_path}':
241{e}"
242 )
243 })?)
244 };
245
246 package.service = if final_pkg_service.is_empty() {
247 None
248 } else {
249 Some(
250 lua.from_value(Value::Table(final_pkg_service))
251 .map_err(|e| {
252 anyhow!(
253 "Failed to parse 'service' block in package file \
254 '{file_path}':
255{e}"
256 )
257 })?
258 )
259 };
260
261 Ok(package)
262}
263
264pub fn parse_lua_package(
271 file_path: &str,
272 version_override: Option<&str>,
273 scope: Option<types::Scope>,
274 quiet: bool
275) -> Result<types::Package> {
276 let platform = utils::get_platform()?;
277 parse_lua_package_for_platform(
278 file_path,
279 &platform,
280 version_override,
281 scope,
282 quiet
283 )
284}
285
286fn extract_line_number(error: &str) -> Option<u32> {
288 if let Some(idx) = error.find("]:") {
290 let after_bracket = &error[idx + 2..];
291 if let Some(colon_idx) = after_bracket.find(':') {
292 let line_str = &after_bracket[..colon_idx];
293 return line_str.parse::<u32>().ok();
294 }
295 }
296 None
297}