Skip to main content

zoi_lua/api/
fs.rs

1use mlua::{self, Lua, Table};
2use std::path::Path;
3use zoi_core::utils;
4
5use std::fs;
6use walkdir::WalkDir;
7/// Exposes filesystem and staging utilities to the Lua environment.
8///
9/// This module provides the "Staging Engine" for Zoi packages. Key functions include:
10/// - `zcp`: Stages files and directories into the `STAGING_DIR` using origin-aware placeholders.
11/// - `zln`: Records symbolic link creation to be performed during the final installation.
12/// - `zmkdir`: Records directory creation.
13/// - `zchmod`/`zchown`: Records metadata changes for staged files.
14///
15/// These functions do not always perform immediate actions; instead, they often record
16/// operations into `__ZoiBuildOperations` for the Rust engine to execute atomically
17/// during the staging-to-store move.
18pub fn add_file_util(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
19    let file_fn = lua.create_function(
20        move |_, (url, path): (String, String)| -> Result<(), mlua::Error> {
21            super::download::download_with_progress(&url, Path::new(&path), quiet)
22        },
23    )?;
24
25    let utils_table: Table = lua.globals().get("UTILS")?;
26    utils_table.set("FILE", file_fn)?;
27
28    Ok(())
29}
30
31pub fn add_zcp(lua: &Lua) -> Result<(), mlua::Error> {
32    let zcp_fn = lua.create_function(|lua, (source, destination): (String, String)| {
33        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
34            Ok(t) => t,
35            Err(_) => {
36                let new_t = lua.create_table()?;
37                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
38                new_t
39            }
40        };
41        let op = lua.create_table()?;
42        op.set("op", "zcp")?;
43        op.set("source", source)?;
44        op.set("destination", destination)?;
45        ops_table.push(op)?;
46        Ok(())
47    })?;
48    lua.globals().set("zcp", zcp_fn)?;
49    Ok(())
50}
51
52pub fn add_zlicense(lua: &Lua) -> Result<(), mlua::Error> {
53    let zlicense_fn = lua.create_function(|lua, source: String| {
54        let zoi_table: Table = lua.globals().get("ZOI")?;
55        let scope: String = zoi_table
56            .get("scope")
57            .unwrap_or_else(|_| "user".to_string());
58        let pkg_table: Table = lua.globals().get("PKG")?;
59        let pkg_name: String = pkg_table
60            .get("name")
61            .unwrap_or_else(|_| "unknown".to_string());
62
63        let filename = Path::new(&source)
64            .file_name()
65            .and_then(|n| n.to_str())
66            .unwrap_or("LICENSE");
67
68        let destination = if scope == "system" {
69            format!("${{usrroot}}/usr/share/licenses/{}/{}", pkg_name, filename)
70        } else {
71            format!("${{pkgstore}}/{}", filename)
72        };
73
74        let zcp: mlua::Function = lua.globals().get("zcp")?;
75        zcp.call::<()>((source, destination))?;
76        Ok(())
77    })?;
78    lua.globals().set("zlicense", zlicense_fn)?;
79    Ok(())
80}
81
82pub fn add_zdoc(lua: &Lua) -> Result<(), mlua::Error> {
83    let zdoc_fn = lua.create_function(|lua, source: String| {
84        let zoi_table: Table = lua.globals().get("ZOI")?;
85        let scope: String = zoi_table
86            .get("scope")
87            .unwrap_or_else(|_| "user".to_string());
88        let pkg_table: Table = lua.globals().get("PKG")?;
89        let pkg_name: String = pkg_table
90            .get("name")
91            .unwrap_or_else(|_| "unknown".to_string());
92
93        let filename = Path::new(&source)
94            .file_name()
95            .and_then(|n| n.to_str())
96            .ok_or_else(|| mlua::Error::RuntimeError("Invalid source path".to_string()))?;
97
98        let destination = if scope == "system" {
99            format!("${{usrroot}}/usr/share/doc/{}/{}", pkg_name, filename)
100        } else {
101            format!("${{pkgstore}}/doc/{}", filename)
102        };
103
104        let zcp: mlua::Function = lua.globals().get("zcp")?;
105        zcp.call::<()>((source, destination))?;
106        Ok(())
107    })?;
108    lua.globals().set("zdoc", zdoc_fn)?;
109    Ok(())
110}
111
112pub fn add_zman(lua: &Lua) -> Result<(), mlua::Error> {
113    let zman_fn = lua.create_function(|lua, (source, section): (String, Option<String>)| {
114        let zoi_table: Table = lua.globals().get("ZOI")?;
115        let scope: String = zoi_table
116            .get("scope")
117            .unwrap_or_else(|_| "user".to_string());
118
119        let path = Path::new(&source);
120        let filename = path
121            .file_name()
122            .and_then(|n| n.to_str())
123            .ok_or_else(|| mlua::Error::RuntimeError("Invalid source path for zman".to_string()))?;
124
125        let inferred_section = if let Some(s) = section {
126            s
127        } else {
128            // Try to infer from extension (e.g. .1, .5, .1.gz, .5.bz2)
129            let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
130            let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
131
132            if ext.parse::<u8>().is_ok() {
133                ext.to_string()
134            } else if (ext == "gz" || ext == "bz2" || ext == "xz") && !stem.is_empty() {
135                let inner_ext = Path::new(stem)
136                    .extension()
137                    .and_then(|e| e.to_str())
138                    .unwrap_or("");
139                if inner_ext.parse::<u8>().is_ok() {
140                    inner_ext.to_string()
141                } else {
142                    "1".to_string()
143                }
144            } else {
145                "1".to_string()
146            }
147        };
148
149        let destination = if scope == "system" {
150            format!(
151                "${{usrroot}}/usr/share/man/man{}/{}",
152                inferred_section, filename
153            )
154        } else {
155            format!("${{pkgstore}}/man/man{}/{}", inferred_section, filename)
156        };
157
158        let zcp: mlua::Function = lua.globals().get("zcp")?;
159        zcp.call::<()>((source, destination))?;
160        Ok(())
161    })?;
162    lua.globals().set("zman", zman_fn)?;
163    Ok(())
164}
165
166pub fn add_zshell(lua: &Lua) -> Result<(), mlua::Error> {
167    let zshell_fn = lua.create_function(|lua, (source, shell): (String, String)| {
168        let filename = Path::new(&source)
169            .file_name()
170            .and_then(|n| n.to_str())
171            .ok_or_else(|| mlua::Error::RuntimeError("Invalid source path for zshell".to_string()))?
172            .to_string();
173
174        let destination = format!("${{pkgstore}}/shell/{}/{}", shell, filename);
175        let zcp: mlua::Function = lua.globals().get("zcp")?;
176        zcp.call::<()>((source, destination.clone()))?;
177
178        let shells_table: Table = match lua.globals().get("__ZoiPackageShells") {
179            Ok(t) => t,
180            Err(_) => {
181                let new_t = lua.create_table()?;
182                lua.globals().set("__ZoiPackageShells", new_t.clone())?;
183                new_t
184            }
185        };
186
187        let shell_files: Vec<String> = shells_table.get(&*shell).unwrap_or_default();
188        let mut shell_files = shell_files;
189        shell_files.push(filename);
190        shells_table.set(shell, shell_files)?;
191
192        Ok(())
193    })?;
194    lua.globals().set("zshell", zshell_fn)?;
195    Ok(())
196}
197
198pub fn add_zsed(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
199    let zsed_fn = lua.create_function(
200        move |lua, (pattern, replacement, file): (String, String, String)| {
201            let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
202            let path = Path::new(&build_dir_str).join(&file);
203
204            let content = std::fs::read_to_string(&path).map_err(|e| {
205                mlua::Error::RuntimeError(format!("Failed to read {}: {}", file, e))
206            })?;
207
208            let re = regex::Regex::new(&pattern).map_err(|e| {
209                mlua::Error::RuntimeError(format!("Invalid regex '{}': {}", pattern, e))
210            })?;
211
212            let new_content = re.replace_all(&content, replacement.as_str());
213
214            std::fs::write(&path, new_content.as_bytes()).map_err(|e| {
215                mlua::Error::RuntimeError(format!("Failed to write {}: {}", file, e))
216            })?;
217
218            if !quiet {
219                println!("Applied sed replacement to {}", file);
220            }
221
222            Ok(())
223        },
224    )?;
225    lua.globals().set("zsed", zsed_fn)?;
226    Ok(())
227}
228
229pub fn add_zln(lua: &Lua) -> Result<(), mlua::Error> {
230    let zln_fn = lua.create_function(|lua, (target, link): (String, String)| {
231        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
232            Ok(t) => t,
233            Err(_) => {
234                let new_t = lua.create_table()?;
235                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
236                new_t
237            }
238        };
239        let op = lua.create_table()?;
240        op.set("op", "zln")?;
241        op.set("target", target)?;
242        op.set("link", link)?;
243        ops_table.push(op)?;
244        Ok(())
245    })?;
246    lua.globals().set("zln", zln_fn)?;
247    Ok(())
248}
249
250pub fn add_zchmod(lua: &Lua) -> Result<(), mlua::Error> {
251    let zchmod_fn = lua.create_function(|lua, (path, mode): (String, u32)| {
252        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
253            Ok(t) => t,
254            Err(_) => {
255                let new_t = lua.create_table()?;
256                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
257                new_t
258            }
259        };
260        let op = lua.create_table()?;
261        op.set("op", "zchmod")?;
262        op.set("path", path)?;
263        op.set("mode", mode)?;
264        ops_table.push(op)?;
265        Ok(())
266    })?;
267    lua.globals().set("zchmod", zchmod_fn)?;
268    Ok(())
269}
270
271pub fn add_zchown(lua: &Lua) -> Result<(), mlua::Error> {
272    let zchown_fn =
273        lua.create_function(|lua, (path, owner, group): (String, String, String)| {
274            let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
275                Ok(t) => t,
276                Err(_) => {
277                    let new_t = lua.create_table()?;
278                    lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
279                    new_t
280                }
281            };
282            let op = lua.create_table()?;
283            op.set("op", "zchown")?;
284            op.set("path", path)?;
285            op.set("owner", owner)?;
286            op.set("group", group)?;
287            ops_table.push(op)?;
288            Ok(())
289        })?;
290    lua.globals().set("zchown", zchown_fn)?;
291    Ok(())
292}
293
294pub fn add_zmkdir(lua: &Lua) -> Result<(), mlua::Error> {
295    let zmkdir_fn = lua.create_function(|lua, path: String| {
296        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
297            Ok(t) => t,
298            Err(_) => {
299                let new_t = lua.create_table()?;
300                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
301                new_t
302            }
303        };
304        let op = lua.create_table()?;
305        op.set("op", "zmkdir")?;
306        op.set("path", path)?;
307        ops_table.push(op)?;
308        Ok(())
309    })?;
310    lua.globals().set("zmkdir", zmkdir_fn)?;
311    Ok(())
312}
313
314pub fn add_zrm(lua: &Lua) -> Result<(), mlua::Error> {
315    let zrm_fn = lua.create_function(|lua, path: String| {
316        let ops_table: Table = match lua.globals().get("__ZoiUninstallOperations") {
317            Ok(t) => t,
318            Err(_) => {
319                let new_t = lua.create_table()?;
320                lua.globals()
321                    .set("__ZoiUninstallOperations", new_t.clone())?;
322                new_t
323            }
324        };
325        let op = lua.create_table()?;
326        op.set("op", "zrm")?;
327        op.set("path", path)?;
328        ops_table.push(op)?;
329        Ok(())
330    })?;
331    lua.globals().set("zrm", zrm_fn)?;
332    Ok(())
333}
334
335pub fn add_fs_util(lua: &Lua) -> Result<(), mlua::Error> {
336    let fs_table = lua.create_table()?;
337
338    let exists_fn = lua.create_function(|lua, path: String| {
339        let p = Path::new(&path);
340        if p.exists() {
341            return Ok(true);
342        }
343        if let Ok(build_dir) = lua.globals().get::<String>("BUILD_DIR")
344            && Path::new(&build_dir).join(p).exists()
345        {
346            return Ok(true);
347        }
348        Ok(false)
349    })?;
350    fs_table.set("exists", exists_fn)?;
351
352    let copy_fn = lua.create_function(|_, (src, dest): (String, String)| {
353        let src_path = Path::new(&src);
354        let dest_path = Path::new(&dest);
355        if src_path.is_dir() {
356            utils::copy_dir_all(src_path, dest_path)
357                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
358        } else {
359            fs::copy(src_path, dest_path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
360        }
361        Ok(true)
362    })?;
363    fs_table.set("copy", copy_fn)?;
364
365    let move_fn = lua.create_function(|_, (src, dest): (String, String)| {
366        fs::rename(src, dest).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
367        Ok(true)
368    })?;
369    fs_table.set("move", move_fn)?;
370
371    let chmod_fn = lua.create_function(|_, (path, mode): (String, u32)| {
372        #[cfg(unix)]
373        {
374            use std::os::unix::fs::PermissionsExt;
375            fs::set_permissions(path, fs::Permissions::from_mode(mode))
376                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
377        }
378        #[cfg(windows)]
379        {
380            let _ = (path, mode);
381        }
382        Ok(true)
383    })?;
384    fs_table.set("chmod", chmod_fn)?;
385
386    let utils_table: Table = lua.globals().get("UTILS")?;
387    utils_table.set("FS", fs_table)?;
388
389    Ok(())
390}
391
392pub fn add_find_util(lua: &Lua) -> Result<(), mlua::Error> {
393    let find_table = lua.create_table()?;
394
395    let find_file_fn = lua.create_function(|lua, (dir, name): (String, String)| {
396        let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
397        let search_dir = Path::new(&build_dir_str).join(dir);
398        for entry in WalkDir::new(search_dir) {
399            let entry = entry.map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
400            if entry.file_name().to_string_lossy() == name {
401                let path = entry.path();
402                let relative_path = path.strip_prefix(Path::new(&build_dir_str)).map_err(|e| {
403                    mlua::Error::RuntimeError(format!(
404                        "Failed to determine relative path for {:?}: {}",
405                        path, e
406                    ))
407                })?;
408                return Ok(Some(relative_path.to_string_lossy().to_string()));
409            }
410        }
411        Ok(None)
412    })?;
413    find_table.set("file", find_file_fn)?;
414
415    let utils_table: Table = lua.globals().get("UTILS")?;
416    utils_table.set("FIND", find_table)?;
417
418    Ok(())
419}