Skip to main content

zoi_lua/api/
fs.rs

1use std::fs;
2use std::path::Path;
3
4use mlua::{self, Lua, Table};
5use walkdir::WalkDir;
6use zoi_core::utils;
7/// Exposes filesystem and staging utilities to the Lua environment.
8///
9/// This module provides the "Staging Engine" for Zoi packages. Key functions
10/// include:
11/// - `zcp`: Stages files and directories into the `STAGING_DIR` using
12///   origin-aware placeholders.
13/// - `zln`: Records symbolic link creation to be performed during the final
14///   installation.
15/// - `zmkdir`: Records directory creation.
16/// - `zchmod`/`zchown`: Records metadata changes for staged files.
17///
18/// These functions do not always perform immediate actions; instead, they often
19/// record operations into `__ZoiBuildOperations` for the Rust engine to execute
20/// atomically during the staging-to-store move.
21/// Adds file downloading utilities to the Lua environment.
22///
23/// # Errors
24///
25/// Returns an error if the `UTILS` table cannot be found or if setting the
26/// `FILE` function fails.
27pub fn add_file_util(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
28    let file_fn = lua.create_function(
29        move |_, (url, path): (String, String)| -> Result<(), mlua::Error> {
30            super::download::download_with_progress(
31                &url,
32                Path::new(&path),
33                quiet
34            )
35        }
36    )?;
37
38    let utils_table: Table = lua.globals().get("UTILS")?;
39    utils_table.set("FILE", file_fn)?;
40
41    Ok(())
42}
43
44/// Adds the `zcp` function to the Lua environment for staging files.
45///
46/// # Errors
47///
48/// Returns an error if the `zcp` function cannot be set in the global
49/// environment.
50pub fn add_zcp(lua: &Lua) -> Result<(), mlua::Error> {
51    let zcp_fn =
52        lua.create_function(|lua, (source, destination): (String, String)| {
53            let ops_table: Table =
54                if let Ok(t) = lua.globals().get("__ZoiBuildOperations") {
55                    t
56                } else {
57                    let new_t = lua.create_table()?;
58                    lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
59                    new_t
60                };
61            let op = lua.create_table()?;
62            op.set("op", "zcp")?;
63            op.set("source", source)?;
64            op.set("destination", destination)?;
65            ops_table.push(op)?;
66            Ok(())
67        })?;
68    lua.globals().set("zcp", zcp_fn)?;
69    Ok(())
70}
71
72/// Adds the `zlicense` function to the Lua environment for staging licenses.
73///
74/// # Errors
75///
76/// Returns an error if the `zlicense` function cannot be set in the global
77/// environment.
78pub fn add_zlicense(lua: &Lua) -> Result<(), mlua::Error> {
79    let zlicense_fn = lua.create_function(|lua, source: String| {
80        let zoi_table: Table = lua.globals().get("ZOI")?;
81        let scope: String = zoi_table
82            .get("scope")
83            .unwrap_or_else(|_| "user".to_string());
84        let pkg_table: Table = lua.globals().get("PKG")?;
85        let pkg_name: String = pkg_table
86            .get("name")
87            .unwrap_or_else(|_| "unknown".to_string());
88
89        let filename = Path::new(&source)
90            .file_name()
91            .and_then(|n| n.to_str())
92            .unwrap_or("LICENSE");
93
94        let destination = if scope == "system" {
95            format!("${{usrroot}}/usr/share/licenses/{pkg_name}/{filename}")
96        } else {
97            format!("${{pkgstore}}/{filename}")
98        };
99
100        let zcp: mlua::Function = lua.globals().get("zcp")?;
101        zcp.call::<()>((source, destination))?;
102        Ok(())
103    })?;
104    lua.globals().set("zlicense", zlicense_fn)?;
105    Ok(())
106}
107
108/// Adds the `zdoc` function to the Lua environment for staging documentation
109/// files.
110///
111/// # Errors
112///
113/// Returns an error if the `zdoc` function cannot be set in the global
114/// environment.
115pub fn add_zdoc(lua: &Lua) -> Result<(), mlua::Error> {
116    let zdoc_fn = lua.create_function(|lua, source: String| {
117        let zoi_table: Table = lua.globals().get("ZOI")?;
118        let scope: String = zoi_table
119            .get("scope")
120            .unwrap_or_else(|_| "user".to_string());
121        let pkg_table: Table = lua.globals().get("PKG")?;
122        let pkg_name: String = pkg_table
123            .get("name")
124            .unwrap_or_else(|_| "unknown".to_string());
125
126        let filename = Path::new(&source)
127            .file_name()
128            .and_then(|n| n.to_str())
129            .ok_or_else(|| {
130                mlua::Error::RuntimeError("Invalid source path".to_string())
131            })?;
132
133        let destination = if scope == "system" {
134            format!("${{usrroot}}/usr/share/doc/{pkg_name}/{filename}")
135        } else {
136            format!("${{pkgstore}}/doc/{filename}")
137        };
138
139        let zcp: mlua::Function = lua.globals().get("zcp")?;
140        zcp.call::<()>((source, destination))?;
141        Ok(())
142    })?;
143    lua.globals().set("zdoc", zdoc_fn)?;
144    Ok(())
145}
146
147/// Adds the `zman` function to the Lua environment for staging manual pages.
148///
149/// # Errors
150///
151/// Returns an error if the `zman` function cannot be set in the global
152/// environment.
153pub fn add_zman(lua: &Lua) -> Result<(), mlua::Error> {
154    let zman_fn = lua.create_function(
155        |lua, (source, section): (String, Option<String>)| {
156            let zoi_table: Table = lua.globals().get("ZOI")?;
157            let scope: String = zoi_table
158                .get("scope")
159                .unwrap_or_else(|_| "user".to_string());
160
161            let path = Path::new(&source);
162            let mut source_paths = Vec::new();
163
164            if path.is_dir() {
165                // Read the directory and collect all files
166                if let Ok(entries) = fs::read_dir(path) {
167                    for entry in entries.flatten() {
168                        if entry.file_type().is_ok_and(|t| t.is_file()) {
169                            source_paths.push(entry.path());
170                        }
171                    }
172                }
173            } else {
174                source_paths.push(path.to_path_buf());
175            }
176
177            for p in source_paths {
178                let filename = p
179                    .file_name()
180                    .and_then(|n| n.to_str())
181                    .ok_or_else(|| {
182                        mlua::Error::RuntimeError(
183                            "Invalid source path for zman".to_string()
184                        )
185                    })?;
186
187                let inferred_section = if let Some(ref s) = section {
188                    s.clone()
189                } else {
190                    // Try to infer from extension (e.g. .1, .5, .1.gz, .5.bz2)
191                    let stem =
192                        p.file_stem().and_then(|s| s.to_str()).unwrap_or("");
193                    let ext =
194                        p.extension().and_then(|e| e.to_str()).unwrap_or("");
195
196                    if ext.parse::<u8>().is_ok() {
197                        ext.to_string()
198                    } else if (ext == "gz" || ext == "bz2" || ext == "xz")
199                        && !stem.is_empty()
200                    {
201                        let inner_ext = Path::new(stem)
202                            .extension()
203                            .and_then(|e| e.to_str())
204                            .unwrap_or("");
205                        if inner_ext.parse::<u8>().is_ok() {
206                            inner_ext.to_string()
207                        } else {
208                            "1".to_string()
209                        }
210                    } else {
211                        "1".to_string()
212                    }
213                };
214
215                let destination = if scope == "system" {
216                    format!(
217                        "${{usrroot}}/usr/share/man/man{inferred_section}/\
218                         {filename}"
219                    )
220                } else {
221                    format!(
222                        "${{pkgstore}}/man/man{inferred_section}/{filename}"
223                    )
224                };
225
226                let zcp: mlua::Function = lua.globals().get("zcp")?;
227                zcp.call::<()>((p.to_string_lossy().to_string(), destination))?;
228            }
229            Ok(())
230        }
231    )?;
232    lua.globals().set("zman", zman_fn)?;
233    Ok(())
234}
235
236/// Adds the `zshell` function to the Lua environment for staging shell
237/// completions.
238///
239/// # Errors
240///
241/// Returns an error if the `zshell` function cannot be set in the global
242/// environment.
243pub fn add_zshell(lua: &Lua) -> Result<(), mlua::Error> {
244    let zshell_fn =
245        lua.create_function(|lua, (source, shell): (String, String)| {
246            let filename = Path::new(&source)
247                .file_name()
248                .and_then(|n| n.to_str())
249                .ok_or_else(|| {
250                    mlua::Error::RuntimeError(
251                        "Invalid source path for zshell".to_string()
252                    )
253                })?
254                .to_string();
255
256            let destination = format!("${{pkgstore}}/shell/{shell}/{filename}");
257            let zcp: mlua::Function = lua.globals().get("zcp")?;
258            zcp.call::<()>((source, destination.clone()))?;
259
260            let shells_table: Table =
261                if let Ok(t) = lua.globals().get("__ZoiPackageShells") {
262                    t
263                } else {
264                    let new_t = lua.create_table()?;
265                    lua.globals().set("__ZoiPackageShells", new_t.clone())?;
266                    new_t
267                };
268
269            let shell_files: Vec<String> =
270                shells_table.get(&*shell).unwrap_or_default();
271            let mut shell_files = shell_files;
272            shell_files.push(filename);
273            shells_table.set(shell, shell_files)?;
274
275            Ok(())
276        })?;
277    lua.globals().set("zshell", zshell_fn)?;
278    Ok(())
279}
280
281/// Adds the `zsed` function to the Lua environment for text replacements in
282/// files.
283///
284/// # Errors
285///
286/// Returns an error if the `zsed` function cannot be set in the global
287/// environment.
288pub fn add_zsed(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
289    let zsed_fn = lua.create_function(
290        move |lua, (pattern, replacement, file): (String, String, String)| {
291            let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
292            let path = Path::new(&build_dir_str).join(&file);
293
294            let content = std::fs::read_to_string(&path).map_err(|e| {
295                mlua::Error::RuntimeError(format!("Failed to read {file}: {e}"))
296            })?;
297
298            let re = regex::Regex::new(&pattern).map_err(|e| {
299                mlua::Error::RuntimeError(format!(
300                    "Invalid regex '{pattern}': {e}"
301                ))
302            })?;
303
304            let new_content = re.replace_all(&content, replacement.as_str());
305
306            std::fs::write(&path, new_content.as_bytes()).map_err(|e| {
307                mlua::Error::RuntimeError(format!(
308                    "Failed to write {file}: {e}"
309                ))
310            })?;
311
312            if !quiet {
313                println!("Applied sed replacement to {file}");
314            }
315
316            Ok(())
317        }
318    )?;
319    lua.globals().set("zsed", zsed_fn)?;
320    Ok(())
321}
322
323/// Adds the `zln` function to the Lua environment for creating symbolic links.
324///
325/// # Errors
326///
327/// Returns an error if the `zln` function cannot be set in the global
328/// environment.
329pub fn add_zln(lua: &Lua) -> Result<(), mlua::Error> {
330    let zln_fn =
331        lua.create_function(|lua, (target, link): (String, String)| {
332            let ops_table: Table =
333                if let Ok(t) = lua.globals().get("__ZoiBuildOperations") {
334                    t
335                } else {
336                    let new_t = lua.create_table()?;
337                    lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
338                    new_t
339                };
340            let op = lua.create_table()?;
341            op.set("op", "zln")?;
342            op.set("target", target)?;
343            op.set("link", link)?;
344            ops_table.push(op)?;
345            Ok(())
346        })?;
347    lua.globals().set("zln", zln_fn)?;
348    Ok(())
349}
350
351/// Adds the `zchmod` function to the Lua environment for changing file
352/// permissions.
353///
354/// # Errors
355///
356/// Returns an error if the `zchmod` function cannot be set in the global
357/// environment.
358pub fn add_zchmod(lua: &Lua) -> Result<(), mlua::Error> {
359    let zchmod_fn =
360        lua.create_function(|lua, (path, mode): (String, u32)| {
361            let ops_table: Table =
362                if let Ok(t) = lua.globals().get("__ZoiBuildOperations") {
363                    t
364                } else {
365                    let new_t = lua.create_table()?;
366                    lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
367                    new_t
368                };
369            let op = lua.create_table()?;
370            op.set("op", "zchmod")?;
371            op.set("path", path)?;
372            op.set("mode", mode)?;
373            ops_table.push(op)?;
374            Ok(())
375        })?;
376    lua.globals().set("zchmod", zchmod_fn)?;
377    Ok(())
378}
379
380/// Adds the `zchown` function to the Lua environment for changing file
381/// ownership.
382///
383/// # Errors
384///
385/// Returns an error if the `zchown` function cannot be set in the global
386/// environment.
387pub fn add_zchown(lua: &Lua) -> Result<(), mlua::Error> {
388    let zchown_fn = lua.create_function(
389        |lua, (path, owner, group): (String, String, String)| {
390            let ops_table: Table =
391                if let Ok(t) = lua.globals().get("__ZoiBuildOperations") {
392                    t
393                } else {
394                    let new_t = lua.create_table()?;
395                    lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
396                    new_t
397                };
398            let op = lua.create_table()?;
399            op.set("op", "zchown")?;
400            op.set("path", path)?;
401            op.set("owner", owner)?;
402            op.set("group", group)?;
403            ops_table.push(op)?;
404            Ok(())
405        }
406    )?;
407    lua.globals().set("zchown", zchown_fn)?;
408    Ok(())
409}
410
411/// Adds the `zmkdir` function to the Lua environment for creating directories.
412///
413/// # Errors
414///
415/// Returns an error if the `zmkdir` function cannot be set in the global
416/// environment.
417pub fn add_zmkdir(lua: &Lua) -> Result<(), mlua::Error> {
418    let zmkdir_fn = lua.create_function(|lua, path: String| {
419        let ops_table: Table =
420            if let Ok(t) = lua.globals().get("__ZoiBuildOperations") {
421                t
422            } else {
423                let new_t = lua.create_table()?;
424                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
425                new_t
426            };
427        let op = lua.create_table()?;
428        op.set("op", "zmkdir")?;
429        op.set("path", path)?;
430        ops_table.push(op)?;
431        Ok(())
432    })?;
433    lua.globals().set("zmkdir", zmkdir_fn)?;
434    Ok(())
435}
436
437/// Adds the `zrm` function to the Lua environment for removing files during
438/// uninstallation.
439///
440/// # Errors
441///
442/// Returns an error if the `zrm` function cannot be set in the global
443/// environment.
444pub fn add_zrm(lua: &Lua) -> Result<(), mlua::Error> {
445    let zrm_fn = lua.create_function(|lua, path: String| {
446        let ops_table: Table =
447            if let Ok(t) = lua.globals().get("__ZoiUninstallOperations") {
448                t
449            } else {
450                let new_t = lua.create_table()?;
451                lua.globals()
452                    .set("__ZoiUninstallOperations", new_t.clone())?;
453                new_t
454            };
455        let op = lua.create_table()?;
456        op.set("op", "zrm")?;
457        op.set("path", path)?;
458        ops_table.push(op)?;
459        Ok(())
460    })?;
461    lua.globals().set("zrm", zrm_fn)?;
462    Ok(())
463}
464
465/// Adds general filesystem utilities to the `UTILS.FS` table.
466///
467/// # Errors
468///
469/// Returns an error if the `UTILS` table cannot be found or if setting the `FS`
470/// table fails.
471pub fn add_fs_util(lua: &Lua) -> Result<(), mlua::Error> {
472    let fs_table = lua.create_table()?;
473
474    let exists_fn = lua.create_function(|lua, path: String| {
475        let p = Path::new(&path);
476        if p.exists() {
477            return Ok(true);
478        }
479        if let Ok(build_dir) = lua.globals().get::<String>("BUILD_DIR")
480            && Path::new(&build_dir).join(p).exists()
481        {
482            return Ok(true);
483        }
484        Ok(false)
485    })?;
486    fs_table.set("exists", exists_fn)?;
487
488    let copy_fn = lua.create_function(|_, (src, dest): (String, String)| {
489        let src_path = Path::new(&src);
490        let dest_path = Path::new(&dest);
491        if src_path.is_dir() {
492            utils::copy_dir_all(src_path, dest_path)
493                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
494        } else {
495            fs::copy(src_path, dest_path)
496                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
497        }
498        Ok(true)
499    })?;
500    fs_table.set("copy", copy_fn)?;
501
502    let move_fn = lua.create_function(|_, (src, dest): (String, String)| {
503        fs::rename(src, dest)
504            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
505        Ok(true)
506    })?;
507    fs_table.set("move", move_fn)?;
508
509    let chmod_fn = lua.create_function(|_, (path, mode): (String, u32)| {
510        #[cfg(unix)]
511        {
512            use std::os::unix::fs::PermissionsExt;
513            fs::set_permissions(path, fs::Permissions::from_mode(mode))
514                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
515        }
516        #[cfg(windows)]
517        {
518            let _ = (path, mode);
519        }
520        Ok(true)
521    })?;
522    fs_table.set("chmod", chmod_fn)?;
523
524    let utils_table: Table = lua.globals().get("UTILS")?;
525    utils_table.set("FS", fs_table)?;
526
527    Ok(())
528}
529
530/// Adds file finding utilities to the `UTILS.FIND` table.
531///
532/// # Errors
533///
534/// Returns an error if the `UTILS` table cannot be found or if setting the
535/// `FIND` table fails.
536pub fn add_find_util(lua: &Lua) -> Result<(), mlua::Error> {
537    let find_table = lua.create_table()?;
538
539    let find_file_fn =
540        lua.create_function(|lua, (dir, name): (String, String)| {
541            let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
542            let search_dir = Path::new(&build_dir_str).join(dir);
543            for entry in WalkDir::new(search_dir) {
544                let entry = entry
545                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
546                if entry.file_name().to_string_lossy() == name {
547                    let path = entry.path();
548                    let relative_path = path
549                        .strip_prefix(Path::new(&build_dir_str))
550                        .map_err(|e| {
551                            mlua::Error::RuntimeError(format!(
552                                "Failed to determine relative path for {}: {e}",
553                                path.display()
554                            ))
555                        })?;
556                    return Ok(Some(
557                        relative_path.to_string_lossy().to_string()
558                    ));
559                }
560            }
561            Ok(None)
562        })?;
563    find_table.set("file", find_file_fn)?;
564
565    let utils_table: Table = lua.globals().get("UTILS")?;
566    utils_table.set("FIND", find_table)?;
567
568    Ok(())
569}