zoi/pkg/lua/
functions.rs

1use crate::utils;
2use flate2::read::GzDecoder;
3use md5;
4use mlua::{self, Lua, LuaSerdeExt, Table, Value};
5use sequoia_openpgp::{Cert, parse::Parse};
6use serde::Deserialize;
7use sha2::{Digest, Sha256, Sha512};
8use std::io::Read;
9use std::path::PathBuf;
10use std::{fs, path::Path};
11use urlencoding;
12use walkdir::WalkDir;
13use xz2::read::XzDecoder;
14use zip::ZipArchive;
15use zstd::stream::read::Decoder as ZstdDecoder;
16
17fn add_parse_util(lua: &Lua) -> Result<(), mlua::Error> {
18    let parse_table = lua.create_table()?;
19
20    let json_fn = lua.create_function(|lua, json_str: String| {
21        let value: serde_json::Value = serde_json::from_str(&json_str)
22            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
23        lua.to_value(&value)
24    })?;
25    parse_table.set("json", json_fn)?;
26
27    let yaml_fn = lua.create_function(|lua, yaml_str: String| {
28        let value: serde_yaml::Value = serde_yaml::from_str(&yaml_str)
29            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
30        lua.to_value(&value)
31    })?;
32    parse_table.set("yaml", yaml_fn)?;
33
34    let toml_fn = lua.create_function(|lua, toml_str: String| {
35        let value: toml::Value =
36            toml::from_str(&toml_str).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
37        lua.to_value(&value)
38    })?;
39    parse_table.set("toml", toml_fn)?;
40
41    let checksum_fn = lua.create_function(|_, (content, file_name): (String, String)| {
42        for line in content.lines() {
43            let parts: Vec<&str> = line.split_whitespace().collect();
44            if parts.len() == 2 && parts[1] == file_name {
45                return Ok(Some(parts[0].to_string()));
46            }
47        }
48        Ok(None)
49    })?;
50    parse_table.set("checksumFile", checksum_fn)?;
51
52    let utils_table: Table = lua.globals().get("UTILS")?;
53    utils_table.set("PARSE", parse_table)?;
54
55    Ok(())
56}
57
58fn add_fetch_util(lua: &Lua) -> Result<(), mlua::Error> {
59    let fetch_table = lua.create_table()?;
60
61    let fetch_fn = lua.create_function(|_, url: String| -> Result<String, mlua::Error> {
62        let response =
63            reqwest::blocking::get(url).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
64        let text = response
65            .text()
66            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
67        Ok(text)
68    })?;
69    fetch_table.set("url", fetch_fn)?;
70
71    let utils_table: Table = lua.globals().get("UTILS")?;
72    utils_table.set("FETCH", fetch_table)?;
73
74    Ok(())
75}
76
77#[derive(Deserialize)]
78struct GitArgs {
79    repo: String,
80    domain: Option<String>,
81    branch: Option<String>,
82}
83
84fn fetch_json(url: &str) -> Result<serde_json::Value, mlua::Error> {
85    let client = reqwest::blocking::Client::builder()
86        .user_agent("zoi")
87        .build()
88        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
89
90    let response = client
91        .get(url)
92        .send()
93        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
94
95    if !response.status().is_success() {
96        return Err(mlua::Error::RuntimeError(format!(
97            "Request to {} failed with status: {} and body: {}",
98            url,
99            response.status(),
100            response.text().unwrap_or_else(|_| "N/A".to_string())
101        )));
102    }
103
104    let text = response
105        .text()
106        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
107    serde_json::from_str(&text).map_err(|e| mlua::Error::RuntimeError(e.to_string()))
108}
109
110fn add_git_fetch_util(lua: &Lua) -> Result<(), mlua::Error> {
111    let utils_table: Table = lua.globals().get("UTILS")?;
112    let fetch_table: Table = utils_table.get("FETCH")?;
113
114    for provider in ["GITHUB", "GITLAB", "GITEA", "FORGEJO"] {
115        let provider_table = lua.create_table()?;
116        let latest_table = lua.create_table()?;
117
118        for what in ["tag", "release", "commit"] {
119            let get_latest_fn = lua.create_function(move |lua, args: Table| {
120                let git_args: GitArgs = lua
121                    .from_value(Value::Table(args))
122                    .map_err(|e| mlua::Error::RuntimeError(format!("Invalid arguments: {}", e)))?;
123
124                let base_url = match provider {
125                    "GITHUB" => git_args
126                        .domain
127                        .unwrap_or_else(|| "https://api.github.com".to_string()),
128                    "GITLAB" => git_args
129                        .domain
130                        .unwrap_or_else(|| "https://gitlab.com".to_string()),
131                    "GITEA" => git_args
132                        .domain
133                        .unwrap_or_else(|| "https://gitea.com".to_string()),
134                    "FORGEJO" => git_args
135                        .domain
136                        .unwrap_or_else(|| "https://codeberg.org".to_string()),
137                    _ => unreachable!(),
138                };
139
140                let url = match (provider, what) {
141                    ("GITHUB", "tag") => format!("{}/repos/{}/tags", base_url, git_args.repo),
142                    ("GITHUB", "release") => {
143                        format!("{}/repos/{}/releases/latest", base_url, git_args.repo)
144                    }
145                    ("GITHUB", "commit") => format!(
146                        "{}/repos/{}/commits?sha={}",
147                        base_url,
148                        git_args.repo,
149                        git_args.branch.as_deref().unwrap_or("HEAD")
150                    ),
151
152                    ("GITLAB", "tag") => format!(
153                        "{}/api/v4/projects/{}/repository/tags",
154                        base_url,
155                        urlencoding::encode(&git_args.repo)
156                    ),
157                    ("GITLAB", "release") => format!(
158                        "{}/api/v4/projects/{}/releases",
159                        base_url,
160                        urlencoding::encode(&git_args.repo)
161                    ),
162                    ("GITLAB", "commit") => format!(
163                        "{}/api/v4/projects/{}/repository/commits?ref_name={}",
164                        base_url,
165                        urlencoding::encode(&git_args.repo),
166                        git_args.branch.as_deref().unwrap_or("HEAD")
167                    ),
168
169                    ("GITEA" | "FORGEJO", "tag") => {
170                        format!("{}/api/v1/repos/{}/tags", base_url, git_args.repo)
171                    }
172                    ("GITEA" | "FORGEJO", "release") => {
173                        format!(
174                            "{}/api/v1/repos/{}/releases/latest",
175                            base_url, git_args.repo
176                        )
177                    }
178                    ("GITEA" | "FORGEJO", "commit") => format!(
179                        "{}/api/v1/repos/{}/commits?sha={}",
180                        base_url,
181                        git_args.repo,
182                        git_args.branch.as_deref().unwrap_or("HEAD")
183                    ),
184                    _ => unreachable!(),
185                };
186
187                let json = fetch_json(&url)?;
188
189                let result = match (provider, what) {
190                    ("GITHUB", "tag") | ("GITEA", "tag") | ("FORGEJO", "tag") => json
191                        .as_array()
192                        .and_then(|a| a.first())
193                        .and_then(|t| t["name"].as_str()),
194                    ("GITHUB", "release") | ("GITEA", "release") | ("FORGEJO", "release") => {
195                        json["tag_name"].as_str()
196                    }
197                    ("GITHUB", "commit") | ("GITEA", "commit") | ("FORGEJO", "commit") => json
198                        .as_array()
199                        .and_then(|a| a.first())
200                        .and_then(|c| c["sha"].as_str()),
201
202                    ("GITLAB", "tag") => json
203                        .as_array()
204                        .and_then(|a| a.first())
205                        .and_then(|t| t["name"].as_str()),
206                    ("GITLAB", "release") => json
207                        .as_array()
208                        .and_then(|a| a.first())
209                        .and_then(|r| r["tag_name"].as_str()),
210                    ("GITLAB", "commit") => json
211                        .as_array()
212                        .and_then(|a| a.first())
213                        .and_then(|c| c["id"].as_str()),
214                    _ => unreachable!(),
215                };
216
217                result.map(|s| s.to_string()).ok_or_else(|| {
218                    mlua::Error::RuntimeError(
219                        "Could not extract value from API response".to_string(),
220                    )
221                })
222            })?;
223            latest_table.set(what, get_latest_fn)?;
224        }
225
226        provider_table.set("LATEST", latest_table)?;
227        fetch_table.set(provider, provider_table)?;
228    }
229
230    Ok(())
231}
232
233fn add_file_util(lua: &Lua) -> Result<(), mlua::Error> {
234    let file_fn = lua.create_function(
235        |_, (url, path): (String, String)| -> Result<(), mlua::Error> {
236            let response = reqwest::blocking::get(url)
237                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
238            let content = response
239                .bytes()
240                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
241            fs::write(path, content).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
242            Ok(())
243        },
244    )?;
245
246    let utils_table: Table = lua.globals().get("UTILS")?;
247    utils_table.set("FILE", file_fn)?;
248
249    Ok(())
250}
251
252fn add_import_util(lua: &Lua, current_path: &Path) -> Result<(), mlua::Error> {
253    let current_path_buf = current_path.to_path_buf();
254    let import_fn = lua.create_function(move |lua, file_name: String| {
255        let path = current_path_buf.parent().unwrap().join(&file_name);
256        let content =
257            fs::read_to_string(&path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
258
259        if let Some(extension) = path.extension().and_then(|s| s.to_str()) {
260            match extension {
261                "json" => {
262                    let value: serde_json::Value = serde_json::from_str(&content)
263                        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
264                    return lua.to_value(&value);
265                }
266                "yaml" | "yml" => {
267                    let value: serde_yaml::Value = serde_yaml::from_str(&content)
268                        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
269                    return lua.to_value(&value);
270                }
271                "toml" => {
272                    let value: toml::Value = toml::from_str(&content)
273                        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
274                    return lua.to_value(&value);
275                }
276                _ => {
277                    return lua.to_value(&content);
278                }
279            }
280        }
281
282        lua.to_value(&content)
283    })?;
284    lua.globals().set("IMPORT", import_fn)?;
285    Ok(())
286}
287
288fn add_include_util(lua: &Lua, current_path: &Path) -> Result<(), mlua::Error> {
289    let current_path_buf = current_path.to_path_buf();
290    let include_fn =
291        lua.create_function(move |lua, file_name: String| -> Result<(), mlua::Error> {
292            let path = current_path_buf.parent().unwrap().join(file_name);
293            let code =
294                fs::read_to_string(path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
295            lua.load(&code).exec()?;
296            Ok(())
297        })?;
298    lua.globals().set("INCLUDE", include_fn)?;
299    Ok(())
300}
301
302fn add_zcp(lua: &Lua) -> Result<(), mlua::Error> {
303    let zcp_fn = lua.create_function(|lua, (source, destination): (String, String)| {
304        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
305            Ok(t) => t,
306            Err(_) => {
307                let new_t = lua.create_table()?;
308                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
309                new_t
310            }
311        };
312        let op = lua.create_table()?;
313        op.set("op", "zcp")?;
314        op.set("source", source)?;
315        op.set("destination", destination)?;
316        ops_table.push(op)?;
317        Ok(())
318    })?;
319    lua.globals().set("zcp", zcp_fn)?;
320    Ok(())
321}
322
323fn add_verify_hash(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
324    let verify_hash_fn =
325        lua.create_function(move |_, (file_path, hash_str): (String, String)| {
326            let parts: Vec<&str> = hash_str.splitn(2, '-').collect();
327            if parts.len() != 2 {
328                return Err(mlua::Error::RuntimeError(
329                    "Invalid hash format. Expected 'algo-hash'".to_string(),
330                ));
331            }
332            let algo = parts[0];
333            let expected_hash = parts[1];
334
335            let mut file = fs::File::open(&file_path)
336                .map_err(|e| mlua::Error::RuntimeError(format!("Failed to open file: {}", e)))?;
337            let mut buffer = Vec::new();
338            file.read_to_end(&mut buffer)
339                .map_err(|e| mlua::Error::RuntimeError(format!("Failed to read file: {}", e)))?;
340
341            let actual_hash = match algo {
342                "md5" => {
343                    let digest = md5::compute(&buffer);
344                    format!("{:x}", digest)
345                }
346                "sha256" => {
347                    let mut hasher = Sha256::new();
348                    hasher.update(&buffer);
349                    hex::encode(hasher.finalize())
350                }
351                "sha512" => {
352                    let mut hasher = Sha512::new();
353                    hasher.update(&buffer);
354                    hex::encode(hasher.finalize())
355                }
356                _ => {
357                    return Err(mlua::Error::RuntimeError(format!(
358                        "Unsupported hash algorithm: {}",
359                        algo
360                    )));
361                }
362            };
363
364            if actual_hash.eq_ignore_ascii_case(expected_hash) {
365                Ok(true)
366            } else {
367                if !quiet {
368                    println!(
369                        "Hash mismatch for {}: expected {}, got {}",
370                        file_path, expected_hash, actual_hash
371                    );
372                }
373                Ok(false)
374            }
375        })?;
376    lua.globals().set("verifyHash", verify_hash_fn)?;
377    Ok(())
378}
379
380fn add_zrm(lua: &Lua) -> Result<(), mlua::Error> {
381    let zrm_fn = lua.create_function(|lua, path: String| {
382        let ops_table: Table = match lua.globals().get("__ZoiUninstallOperations") {
383            Ok(t) => t,
384            Err(_) => {
385                let new_t = lua.create_table()?;
386                lua.globals()
387                    .set("__ZoiUninstallOperations", new_t.clone())?;
388                new_t
389            }
390        };
391        let op = lua.create_table()?;
392        op.set("op", "zrm")?;
393        op.set("path", path)?;
394        ops_table.push(op)?;
395        Ok(())
396    })?;
397    lua.globals().set("zrm", zrm_fn)?;
398    Ok(())
399}
400
401fn add_cmd_util(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
402    let cmd_fn = lua.create_function(move |lua, command: String| {
403        let build_dir: String = lua.globals().get("BUILD_DIR")?;
404
405        if !quiet {
406            println!("Executing: {}", command);
407        }
408        let output = if cfg!(target_os = "windows") {
409            std::process::Command::new("pwsh")
410                .arg("-Command")
411                .arg(&command)
412                .current_dir(&build_dir)
413                .output()
414        } else {
415            std::process::Command::new("bash")
416                .arg("-c")
417                .arg(&command)
418                .current_dir(&build_dir)
419                .output()
420        };
421
422        match output {
423            Ok(out) => {
424                if !out.status.success() {
425                    if !quiet {
426                        eprintln!("[cmd] {}", String::from_utf8_lossy(&out.stderr));
427                    }
428                    Ok(String::new())
429                } else {
430                    Ok(String::from_utf8_lossy(&out.stdout).to_string())
431                }
432            }
433            Err(e) => {
434                if !quiet {
435                    eprintln!("[cmd] Failed to execute command: {}", e);
436                }
437                Ok(String::new())
438            }
439        }
440    })?;
441    lua.globals().set("cmd", cmd_fn)?;
442    Ok(())
443}
444
445fn add_fs_util(lua: &Lua) -> Result<(), mlua::Error> {
446    let fs_table = lua.create_table()?;
447
448    let exists_fn = lua.create_function(|_, path: String| Ok(Path::new(&path).exists()))?;
449    fs_table.set("exists", exists_fn)?;
450
451    let utils_table: Table = lua.globals().get("UTILS")?;
452    utils_table.set("FS", fs_table)?;
453
454    Ok(())
455}
456
457fn add_find_util(lua: &Lua) -> Result<(), mlua::Error> {
458    let find_table = lua.create_table()?;
459
460    let find_file_fn = lua.create_function(|lua, (dir, name): (String, String)| {
461        let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
462        let search_dir = Path::new(&build_dir_str).join(dir);
463        for entry in WalkDir::new(search_dir) {
464            let entry = entry.map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
465            if entry.file_name().to_string_lossy() == name {
466                let path = entry.path();
467                let relative_path = path.strip_prefix(Path::new(&build_dir_str)).unwrap();
468                return Ok(Some(relative_path.to_string_lossy().to_string()));
469            }
470        }
471        Ok(None)
472    })?;
473    find_table.set("file", find_file_fn)?;
474
475    let utils_table: Table = lua.globals().get("UTILS")?;
476    utils_table.set("FIND", find_table)?;
477
478    Ok(())
479}
480
481fn add_extract_util(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
482    let extract_fn =
483        lua.create_function(move |lua, (source, out_name): (String, Option<String>)| {
484            let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
485            let build_dir = Path::new(&build_dir_str);
486
487            let archive_file = if source.starts_with("http") {
488                if !quiet {
489                    println!("Downloading: {}", source);
490                }
491                let file_name = source.split('/').next_back().unwrap_or("download.tmp");
492                let temp_path = build_dir.join(file_name);
493                let response = reqwest::blocking::get(&source)
494                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
495                let content = response
496                    .bytes()
497                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
498                fs::write(&temp_path, content)
499                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
500                temp_path
501            } else {
502                PathBuf::from(source)
503            };
504
505            let out_dir_name = out_name.unwrap_or_else(|| "extracted".to_string());
506            let out_dir = build_dir.join(out_dir_name);
507            fs::create_dir_all(&out_dir).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
508
509            if !quiet {
510                println!(
511                    "Extracting {} to {}",
512                    archive_file.display(),
513                    out_dir.display()
514                );
515            }
516
517            let file = fs::File::open(&archive_file)
518                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
519
520            let archive_path_str = archive_file.to_string_lossy();
521
522            if archive_path_str.ends_with(".zip") {
523                let mut archive =
524                    ZipArchive::new(file).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
525                archive
526                    .extract(&out_dir)
527                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
528            } else if archive_path_str.ends_with(".tar.gz") {
529                let tar_gz = GzDecoder::new(file);
530                let mut archive = tar::Archive::new(tar_gz);
531                archive
532                    .unpack(&out_dir)
533                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
534            } else if archive_path_str.ends_with(".tar.zst") {
535                let tar_zst =
536                    ZstdDecoder::new(file).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
537                let mut archive = tar::Archive::new(tar_zst);
538                archive
539                    .unpack(&out_dir)
540                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
541            } else if archive_path_str.ends_with(".tar.xz") {
542                let tar_xz = XzDecoder::new(file);
543                let mut archive = tar::Archive::new(tar_xz);
544                archive
545                    .unpack(&out_dir)
546                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
547            } else {
548                return Err(mlua::Error::RuntimeError(format!(
549                    "Unsupported archive format for file: {}",
550                    archive_path_str
551                )));
552            }
553
554            Ok(())
555        })?;
556
557    let utils_table: Table = lua.globals().get("UTILS")?;
558    utils_table.set("EXTRACT", extract_fn)?;
559
560    Ok(())
561}
562
563fn add_verify_signature(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
564    let verify_sig_fn = lua.create_function(
565        move |_, (file_path, sig_path, key_source): (String, String, String)| {
566            let key_bytes: Vec<u8> = if key_source.starts_with("http") {
567                match reqwest::blocking::get(&key_source).and_then(|r| r.bytes()) {
568                    Ok(b) => b.to_vec(),
569                    Err(e) => {
570                        return Err(mlua::Error::RuntimeError(format!(
571                            "Failed to download key: {}",
572                            e
573                        )));
574                    }
575                }
576            } else if Path::new(&key_source).exists() {
577                match fs::read(&key_source) {
578                    Ok(b) => b,
579                    Err(e) => {
580                        return Err(mlua::Error::RuntimeError(format!(
581                            "Failed to read key file: {}",
582                            e
583                        )));
584                    }
585                }
586            } else {
587                let pgp_dir = match crate::pkg::pgp::get_pgp_dir() {
588                    Ok(dir) => dir,
589                    Err(e) => {
590                        return Err(mlua::Error::RuntimeError(format!(
591                            "Failed to get PGP dir: {}",
592                            e
593                        )));
594                    }
595                };
596                let key_path = pgp_dir.join(format!("{}.asc", key_source));
597                if !key_path.exists() {
598                    return Err(mlua::Error::RuntimeError(format!(
599                        "Key with name '{}' not found.",
600                        key_source
601                    )));
602                }
603                match fs::read(&key_path) {
604                    Ok(b) => b,
605                    Err(e) => {
606                        return Err(mlua::Error::RuntimeError(format!(
607                            "Failed to read key file: {}",
608                            e
609                        )));
610                    }
611                }
612            };
613
614            let cert = match Cert::from_bytes(&key_bytes) {
615                Ok(c) => c,
616                Err(e) => return Err(mlua::Error::RuntimeError(format!("Invalid PGP key: {}", e))),
617            };
618
619            let result = crate::pkg::pgp::verify_detached_signature(
620                Path::new(&file_path),
621                Path::new(&sig_path),
622                &cert,
623            );
624
625            match result {
626                Ok(_) => Ok(true),
627                Err(e) => {
628                    if !quiet {
629                        eprintln!("Signature verification failed: {}", e);
630                    }
631                    Ok(false)
632                }
633            }
634        },
635    )?;
636    lua.globals().set("verifySignature", verify_sig_fn)?;
637    Ok(())
638}
639
640fn add_add_pgp_key(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
641    let add_pgp_key_fn = lua.create_function(move |_, (source, name): (String, String)| {
642        let result = if source.starts_with("http") {
643            crate::pkg::pgp::add_key_from_url(&source, &name)
644        } else {
645            crate::pkg::pgp::add_key_from_path(&source, Some(&name))
646        };
647
648        if let Err(e) = result {
649            if !quiet {
650                eprintln!("Failed to add PGP key '{}': {}", name, e);
651            }
652            return Ok(false);
653        }
654        Ok(true)
655    })?;
656    lua.globals().set("addPgpKey", add_pgp_key_fn)?;
657    Ok(())
658}
659
660pub fn add_package_lifecycle_functions(lua: &Lua) -> Result<(), mlua::Error> {
661    let metadata_fn = lua.create_function(move |lua, pkg_def: Table| {
662        if let Ok(meta_table) = lua.globals().get::<Table>("__ZoiPackageMeta")
663            && let Ok(pkg_global) = lua.globals().get::<Table>("PKG")
664        {
665            for pair in pkg_def.pairs::<Value, Value>() {
666                let (key, value) = pair?;
667                meta_table.set(key.clone(), value.clone())?;
668                pkg_global.set(key, value)?;
669            }
670        }
671        Ok(())
672    })?;
673    lua.globals().set("metadata", metadata_fn)?;
674
675    let dependencies_fn = lua.create_function(move |lua, deps_def: Table| {
676        if let Ok(deps_table) = lua.globals().get::<Table>("__ZoiPackageDeps") {
677            for pair in deps_def.pairs::<String, Value>() {
678                let (key, value) = pair?;
679                deps_table.set(key, value)?;
680            }
681        }
682        Ok(())
683    })?;
684    lua.globals().set("dependencies", dependencies_fn)?;
685
686    let updates_fn = lua.create_function(move |lua, updates_list: Table| {
687        if let Ok(updates_table) = lua.globals().get::<Table>("__ZoiPackageUpdates") {
688            for pair in updates_list.pairs::<Value, Table>() {
689                let (_, update_info) = pair?;
690                updates_table.push(update_info)?;
691            }
692        }
693        Ok(())
694    })?;
695    lua.globals().set("updates", updates_fn)?;
696
697    let hooks_fn = lua.create_function(move |lua, hooks_def: Table| {
698        if let Ok(hooks_table) = lua.globals().get::<Table>("__ZoiPackageHooks") {
699            for pair in hooks_def.pairs::<String, Value>() {
700                let (key, value) = pair?;
701                hooks_table.set(key, value)?;
702            }
703        }
704        Ok(())
705    })?;
706    lua.globals().set("hooks", hooks_fn)?;
707
708    let prepare_fn = lua.create_function(|_, _: Table| Ok(()))?;
709    lua.globals().set("prepare", prepare_fn)?;
710    let package_fn = lua.create_function(|_, _: Table| Ok(()))?;
711    lua.globals().set("package", package_fn)?;
712    let verify_fn = lua.create_function(|_, _: Table| Ok(()))?;
713    lua.globals().set("verify", verify_fn)?;
714    let test_fn = lua.create_function(|_, _: Table| Ok(true))?;
715    lua.globals().set("test", test_fn)?;
716    let uninstall_fn = lua.create_function(|_, _: Table| Ok(()))?;
717    lua.globals().set("uninstall", uninstall_fn)?;
718
719    Ok(())
720}
721
722pub fn setup_lua_environment(
723    lua: &Lua,
724    platform: &str,
725    version_override: Option<&str>,
726    file_path: Option<&str>,
727    create_pkg_dir: Option<&str>,
728    sub_package: Option<&str>,
729    quiet: bool,
730) -> Result<(), mlua::Error> {
731    let system_table = lua.create_table()?;
732    let parts: Vec<&str> = platform.split('-').collect();
733    system_table.set("OS", *parts.first().unwrap_or(&""))?;
734    system_table.set("ARCH", *parts.get(1).unwrap_or(&""))?;
735    if let Some(distro) = utils::get_linux_distribution() {
736        system_table.set("DISTRO", distro)?;
737    }
738    if let Some(manager) = utils::get_native_package_manager() {
739        system_table.set("MANAGER", manager)?;
740    }
741    lua.globals().set("SYSTEM", system_table)?;
742
743    let zoi_table = lua.create_table()?;
744    if let Some(ver) = version_override {
745        zoi_table.set("VERSION", ver)?;
746    }
747
748    if let Some(dir) = create_pkg_dir {
749        zoi_table.set("CREATE_PKG_DIR", dir)?;
750    }
751
752    if let Some(sub) = sub_package {
753        lua.globals().set("SUBPKG", sub)?;
754    }
755
756    let path_table = lua.create_table()?;
757    if let Some(home_dir) = home::home_dir() {
758        path_table.set("user", home_dir.join(".zoi").to_string_lossy().to_string())?;
759    }
760
761    let system_bin_path = if cfg!(target_os = "windows") {
762        "C:\\ProgramData\\zoi\\pkgs\\bin".to_string()
763    } else {
764        "/usr/local/bin".to_string()
765    };
766    path_table.set("system", system_bin_path)?;
767
768    zoi_table.set("PATH", path_table)?;
769    lua.globals().set("ZOI", zoi_table)?;
770
771    let utils_table = lua.create_table()?;
772    lua.globals().set("UTILS", utils_table)?;
773
774    add_fetch_util(lua)?;
775    add_parse_util(lua)?;
776    add_git_fetch_util(lua)?;
777    add_file_util(lua)?;
778    add_zcp(lua)?;
779    add_verify_hash(lua, quiet)?;
780    add_zrm(lua)?;
781    add_cmd_util(lua, quiet)?;
782    add_fs_util(lua)?;
783    add_find_util(lua)?;
784    add_extract_util(lua, quiet)?;
785    add_verify_signature(lua, quiet)?;
786    add_add_pgp_key(lua, quiet)?;
787    add_package_lifecycle_functions(lua)?;
788
789    if let Some(path_str) = file_path {
790        let path = Path::new(path_str);
791        add_import_util(lua, path)?;
792        add_include_util(lua, path)?;
793    }
794
795    if quiet {
796        let quiet_print = lua.create_function(|_, _: mlua::MultiValue| Ok(()))?;
797        lua.globals().set("print", quiet_print)?;
798    }
799
800    Ok(())
801}