Skip to main content

zoi_lua/
parser.rs

1use crate::functions;
2use anyhow::{Result, anyhow};
3use mlua::{self, Lua, LuaSerdeExt, Table, Value};
4use std::fs;
5use std::path::Path;
6use tar::Archive;
7use walkdir::WalkDir;
8use zoi_core::{types, utils};
9use zstd::stream::read::Decoder as ZstdDecoder;
10
11pub fn parse_lua_package_from_archive(
12    archive_path: &Path,
13    version_override: Option<&str>,
14    scope: Option<types::Scope>,
15    quiet: bool,
16) -> Result<types::Package> {
17    let platform = utils::get_platform()?;
18    parse_lua_package_from_archive_for_platform(
19        archive_path,
20        &platform,
21        version_override,
22        scope,
23        quiet,
24    )
25}
26
27pub fn parse_lua_package_from_archive_for_platform(
28    archive_path: &Path,
29    platform: &str,
30    version_override: Option<&str>,
31    scope: Option<types::Scope>,
32    quiet: bool,
33) -> Result<types::Package> {
34    let file = fs::File::open(archive_path)?;
35    let decoder = ZstdDecoder::new(file)?;
36    let mut archive = Archive::new(decoder);
37    let temp_dir = tempfile::Builder::new()
38        .prefix("zoi-arch-parse-")
39        .tempdir()?;
40    archive.unpack(temp_dir.path())?;
41
42    let mut pkg_lua = None;
43    for entry in WalkDir::new(temp_dir.path())
44        .into_iter()
45        .filter_map(|e| e.ok())
46    {
47        if entry.file_name().to_string_lossy().ends_with(".pkg.lua") {
48            pkg_lua = Some(entry.path().to_path_buf());
49            break;
50        }
51    }
52
53    let pkg_lua_path = pkg_lua.ok_or_else(|| anyhow!("No .pkg.lua in archive"))?;
54    parse_lua_package_from_file_for_platform(
55        pkg_lua_path
56            .to_str()
57            .ok_or_else(|| anyhow!("Invalid path"))?,
58        platform,
59        version_override,
60        scope,
61        quiet,
62    )
63}
64
65pub fn parse_lua_package_for_platform(
66    file_path: &str,
67    platform: &str,
68    version_override: Option<&str>,
69    scope: Option<types::Scope>,
70    quiet: bool,
71) -> Result<types::Package> {
72    if file_path.ends_with(".zpa") || file_path.ends_with(".zsa") {
73        return parse_lua_package_from_archive_for_platform(
74            Path::new(file_path),
75            platform,
76            version_override,
77            scope,
78            quiet,
79        );
80    }
81    parse_lua_package_from_file_for_platform(file_path, platform, version_override, scope, quiet)
82}
83
84fn parse_lua_package_from_file_for_platform(
85    file_path: &str,
86    platform: &str,
87    version_override: Option<&str>,
88    scope: Option<types::Scope>,
89    quiet: bool,
90) -> Result<types::Package> {
91    let lua_code = fs::read_to_string(file_path)?;
92    let lua = Lua::new();
93
94    let pkg_meta_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
95    let pkg_deps_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
96    let pkg_updates_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
97    let pkg_hooks_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
98    let pkg_service_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
99    lua.globals()
100        .set("__ZoiPackageMeta", pkg_meta_table)
101        .map_err(|e| anyhow!(e.to_string()))?;
102    lua.globals()
103        .set("__ZoiPackageDeps", pkg_deps_table)
104        .map_err(|e| anyhow!(e.to_string()))?;
105    lua.globals()
106        .set("__ZoiPackageUpdates", pkg_updates_table)
107        .map_err(|e| anyhow!(e.to_string()))?;
108    lua.globals()
109        .set("__ZoiPackageHooks", pkg_hooks_table)
110        .map_err(|e| anyhow!(e.to_string()))?;
111    lua.globals()
112        .set("__ZoiPackageService", pkg_service_table)
113        .map_err(|e| anyhow!(e.to_string()))?;
114
115    let pkg_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
116    lua.globals()
117        .set("PKG", pkg_table)
118        .map_err(|e| anyhow!(e.to_string()))?;
119
120    functions::setup_lua_environment(
121        &lua,
122        platform,
123        version_override,
124        Some(file_path),
125        None,
126        None,
127        None,
128        None,
129        scope,
130        quiet,
131    )
132    .map_err(|e| anyhow!("Failed to setup Lua environment for '{}': {}", file_path, e))?;
133
134    lua.load(&lua_code).exec().map_err(|e| {
135        anyhow!(
136            "Failed to execute Lua package file '{}':
137{}",
138            file_path,
139            e
140        )
141    })?;
142
143    let final_pkg_meta: Table = lua
144        .globals()
145        .get("__ZoiPackageMeta")
146        .map_err(|e| anyhow!(e.to_string()))?;
147    let final_pkg_deps: Table = lua
148        .globals()
149        .get("__ZoiPackageDeps")
150        .map_err(|e| anyhow!(e.to_string()))?;
151    let final_pkg_updates: Table = lua
152        .globals()
153        .get("__ZoiPackageUpdates")
154        .map_err(|e| anyhow!(e.to_string()))?;
155    let final_pkg_hooks: Table = lua
156        .globals()
157        .get("__ZoiPackageHooks")
158        .map_err(|e| anyhow!(e.to_string()))?;
159    let final_pkg_service: Table = lua
160        .globals()
161        .get("__ZoiPackageService")
162        .map_err(|e| anyhow!(e.to_string()))?;
163
164    let mut package: types::Package =
165        lua.from_value(Value::Table(final_pkg_meta)).map_err(|e| {
166            anyhow!(
167                "Failed to parse 'metadata' block in package file '{}':
168{}",
169                file_path,
170                e
171            )
172        })?;
173
174    package.dependencies = if final_pkg_deps.is_empty() {
175        None
176    } else {
177        Some(lua.from_value(Value::Table(final_pkg_deps)).map_err(|e| {
178            anyhow!(
179                "Failed to parse 'dependencies' block in package file '{}':
180{}",
181                file_path,
182                e
183            )
184        })?)
185    };
186
187    package.updates = if final_pkg_updates.is_empty() {
188        None
189    } else {
190        Some(
191            lua.from_value(Value::Table(final_pkg_updates))
192                .map_err(|e| {
193                    anyhow!(
194                        "Failed to parse 'updates' block in package file '{}':
195{}",
196                        file_path,
197                        e
198                    )
199                })?,
200        )
201    };
202
203    package.hooks = if final_pkg_hooks.is_empty() {
204        None
205    } else {
206        Some(lua.from_value(Value::Table(final_pkg_hooks)).map_err(|e| {
207            anyhow!(
208                "Failed to parse 'hooks' block in package file '{}':
209{}",
210                file_path,
211                e
212            )
213        })?)
214    };
215
216    package.service = if final_pkg_service.is_empty() {
217        None
218    } else {
219        Some(
220            lua.from_value(Value::Table(final_pkg_service))
221                .map_err(|e| {
222                    anyhow!(
223                        "Failed to parse 'service' block in package file '{}':
224{}",
225                        file_path,
226                        e
227                    )
228                })?,
229        )
230    };
231
232    Ok(package)
233}
234
235pub fn parse_lua_package(
236    file_path: &str,
237    version_override: Option<&str>,
238    scope: Option<types::Scope>,
239    quiet: bool,
240) -> Result<types::Package> {
241    let platform = utils::get_platform()?;
242    parse_lua_package_for_platform(file_path, &platform, version_override, scope, quiet)
243}