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) -> Result<(), mlua::Error> {
19    let file_fn = lua.create_function(
20        |_, (url, path): (String, String)| -> Result<(), mlua::Error> {
21            let client =
22                utils::get_http_client().map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
23            let mut attempt = 0u32;
24            let response = loop {
25                attempt += 1;
26                match client.get(&url).send() {
27                    Ok(resp) => break resp,
28                    Err(e) => {
29                        if attempt < 3 {
30                            eprintln!("Download failed ({}). Retrying...", e);
31                            zoi_core::utils::retry_backoff_sleep(attempt);
32                            continue;
33                        } else {
34                            return Err(mlua::Error::RuntimeError(e.to_string()));
35                        }
36                    }
37                }
38            };
39            let content = response
40                .bytes()
41                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
42            fs::write(path, content).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
43            Ok(())
44        },
45    )?;
46
47    let utils_table: Table = lua.globals().get("UTILS")?;
48    utils_table.set("FILE", file_fn)?;
49
50    Ok(())
51}
52
53pub fn add_zcp(lua: &Lua) -> Result<(), mlua::Error> {
54    let zcp_fn = lua.create_function(|lua, (source, destination): (String, String)| {
55        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
56            Ok(t) => t,
57            Err(_) => {
58                let new_t = lua.create_table()?;
59                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
60                new_t
61            }
62        };
63        let op = lua.create_table()?;
64        op.set("op", "zcp")?;
65        op.set("source", source)?;
66        op.set("destination", destination)?;
67        ops_table.push(op)?;
68        Ok(())
69    })?;
70    lua.globals().set("zcp", zcp_fn)?;
71    Ok(())
72}
73
74pub fn add_zlicense(lua: &Lua) -> Result<(), mlua::Error> {
75    let zlicense_fn = lua.create_function(|lua, source: String| {
76        let destination = "${pkgstore}/LICENSE".to_string();
77        let zcp: mlua::Function = lua.globals().get("zcp")?;
78        zcp.call::<()>((source, destination))?;
79        Ok(())
80    })?;
81    lua.globals().set("zlicense", zlicense_fn)?;
82    Ok(())
83}
84
85pub fn add_zdoc(lua: &Lua) -> Result<(), mlua::Error> {
86    let zdoc_fn = lua.create_function(|lua, source: String| {
87        let filename = Path::new(&source)
88            .file_name()
89            .and_then(|n| n.to_str())
90            .ok_or_else(|| mlua::Error::RuntimeError("Invalid source path".to_string()))?;
91        let destination = format!("${{pkgstore}}/doc/{}", filename);
92        let zcp: mlua::Function = lua.globals().get("zcp")?;
93        zcp.call::<()>((source, destination))?;
94        Ok(())
95    })?;
96    lua.globals().set("zdoc", zdoc_fn)?;
97    Ok(())
98}
99
100pub fn add_zshell(lua: &Lua) -> Result<(), mlua::Error> {
101    let zshell_fn = lua.create_function(|lua, (source, shell): (String, String)| {
102        let filename = Path::new(&source)
103            .file_name()
104            .and_then(|n| n.to_str())
105            .ok_or_else(|| mlua::Error::RuntimeError("Invalid source path for zshell".to_string()))?
106            .to_string();
107
108        let destination = format!("${{pkgstore}}/shell/{}/{}", shell, filename);
109        let zcp: mlua::Function = lua.globals().get("zcp")?;
110        zcp.call::<()>((source, destination.clone()))?;
111
112        let shells_table: Table = match lua.globals().get("__ZoiPackageShells") {
113            Ok(t) => t,
114            Err(_) => {
115                let new_t = lua.create_table()?;
116                lua.globals().set("__ZoiPackageShells", new_t.clone())?;
117                new_t
118            }
119        };
120
121        let shell_files: Vec<String> = shells_table.get(&*shell).unwrap_or_default();
122        let mut shell_files = shell_files;
123        shell_files.push(filename);
124        shells_table.set(shell, shell_files)?;
125
126        Ok(())
127    })?;
128    lua.globals().set("zshell", zshell_fn)?;
129    Ok(())
130}
131
132pub fn add_zsed(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
133    let zsed_fn = lua.create_function(
134        move |lua, (pattern, replacement, file): (String, String, String)| {
135            let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
136            let path = Path::new(&build_dir_str).join(&file);
137
138            let content = std::fs::read_to_string(&path).map_err(|e| {
139                mlua::Error::RuntimeError(format!("Failed to read {}: {}", file, e))
140            })?;
141
142            let re = regex::Regex::new(&pattern).map_err(|e| {
143                mlua::Error::RuntimeError(format!("Invalid regex '{}': {}", pattern, e))
144            })?;
145
146            let new_content = re.replace_all(&content, replacement.as_str());
147
148            std::fs::write(&path, new_content.as_bytes()).map_err(|e| {
149                mlua::Error::RuntimeError(format!("Failed to write {}: {}", file, e))
150            })?;
151
152            if !quiet {
153                println!("Applied sed replacement to {}", file);
154            }
155
156            Ok(())
157        },
158    )?;
159    lua.globals().set("zsed", zsed_fn)?;
160    Ok(())
161}
162
163pub fn add_zln(lua: &Lua) -> Result<(), mlua::Error> {
164    let zln_fn = lua.create_function(|lua, (target, link): (String, String)| {
165        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
166            Ok(t) => t,
167            Err(_) => {
168                let new_t = lua.create_table()?;
169                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
170                new_t
171            }
172        };
173        let op = lua.create_table()?;
174        op.set("op", "zln")?;
175        op.set("target", target)?;
176        op.set("link", link)?;
177        ops_table.push(op)?;
178        Ok(())
179    })?;
180    lua.globals().set("zln", zln_fn)?;
181    Ok(())
182}
183
184pub fn add_zchmod(lua: &Lua) -> Result<(), mlua::Error> {
185    let zchmod_fn = lua.create_function(|lua, (path, mode): (String, u32)| {
186        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
187            Ok(t) => t,
188            Err(_) => {
189                let new_t = lua.create_table()?;
190                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
191                new_t
192            }
193        };
194        let op = lua.create_table()?;
195        op.set("op", "zchmod")?;
196        op.set("path", path)?;
197        op.set("mode", mode)?;
198        ops_table.push(op)?;
199        Ok(())
200    })?;
201    lua.globals().set("zchmod", zchmod_fn)?;
202    Ok(())
203}
204
205pub fn add_zchown(lua: &Lua) -> Result<(), mlua::Error> {
206    let zchown_fn =
207        lua.create_function(|lua, (path, owner, group): (String, String, String)| {
208            let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
209                Ok(t) => t,
210                Err(_) => {
211                    let new_t = lua.create_table()?;
212                    lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
213                    new_t
214                }
215            };
216            let op = lua.create_table()?;
217            op.set("op", "zchown")?;
218            op.set("path", path)?;
219            op.set("owner", owner)?;
220            op.set("group", group)?;
221            ops_table.push(op)?;
222            Ok(())
223        })?;
224    lua.globals().set("zchown", zchown_fn)?;
225    Ok(())
226}
227
228pub fn add_zmkdir(lua: &Lua) -> Result<(), mlua::Error> {
229    let zmkdir_fn = lua.create_function(|lua, path: String| {
230        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
231            Ok(t) => t,
232            Err(_) => {
233                let new_t = lua.create_table()?;
234                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
235                new_t
236            }
237        };
238        let op = lua.create_table()?;
239        op.set("op", "zmkdir")?;
240        op.set("path", path)?;
241        ops_table.push(op)?;
242        Ok(())
243    })?;
244    lua.globals().set("zmkdir", zmkdir_fn)?;
245    Ok(())
246}
247
248pub fn add_zrm(lua: &Lua) -> Result<(), mlua::Error> {
249    let zrm_fn = lua.create_function(|lua, path: String| {
250        let ops_table: Table = match lua.globals().get("__ZoiUninstallOperations") {
251            Ok(t) => t,
252            Err(_) => {
253                let new_t = lua.create_table()?;
254                lua.globals()
255                    .set("__ZoiUninstallOperations", new_t.clone())?;
256                new_t
257            }
258        };
259        let op = lua.create_table()?;
260        op.set("op", "zrm")?;
261        op.set("path", path)?;
262        ops_table.push(op)?;
263        Ok(())
264    })?;
265    lua.globals().set("zrm", zrm_fn)?;
266    Ok(())
267}
268
269pub fn add_fs_util(lua: &Lua) -> Result<(), mlua::Error> {
270    let fs_table = lua.create_table()?;
271
272    let exists_fn = lua.create_function(|lua, path: String| {
273        let p = Path::new(&path);
274        if p.exists() {
275            return Ok(true);
276        }
277        if let Ok(build_dir) = lua.globals().get::<String>("BUILD_DIR")
278            && Path::new(&build_dir).join(p).exists()
279        {
280            return Ok(true);
281        }
282        Ok(false)
283    })?;
284    fs_table.set("exists", exists_fn)?;
285
286    let copy_fn = lua.create_function(|_, (src, dest): (String, String)| {
287        let src_path = Path::new(&src);
288        let dest_path = Path::new(&dest);
289        if src_path.is_dir() {
290            utils::copy_dir_all(src_path, dest_path)
291                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
292        } else {
293            fs::copy(src_path, dest_path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
294        }
295        Ok(true)
296    })?;
297    fs_table.set("copy", copy_fn)?;
298
299    let move_fn = lua.create_function(|_, (src, dest): (String, String)| {
300        fs::rename(src, dest).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
301        Ok(true)
302    })?;
303    fs_table.set("move", move_fn)?;
304
305    let chmod_fn = lua.create_function(|_, (path, mode): (String, u32)| {
306        #[cfg(unix)]
307        {
308            use std::os::unix::fs::PermissionsExt;
309            fs::set_permissions(path, fs::Permissions::from_mode(mode))
310                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
311        }
312        #[cfg(windows)]
313        {
314            let _ = (path, mode);
315        }
316        Ok(true)
317    })?;
318    fs_table.set("chmod", chmod_fn)?;
319
320    let utils_table: Table = lua.globals().get("UTILS")?;
321    utils_table.set("FS", fs_table)?;
322
323    Ok(())
324}
325
326pub fn add_find_util(lua: &Lua) -> Result<(), mlua::Error> {
327    let find_table = lua.create_table()?;
328
329    let find_file_fn = lua.create_function(|lua, (dir, name): (String, String)| {
330        let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
331        let search_dir = Path::new(&build_dir_str).join(dir);
332        for entry in WalkDir::new(search_dir) {
333            let entry = entry.map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
334            if entry.file_name().to_string_lossy() == name {
335                let path = entry.path();
336                let relative_path = path.strip_prefix(Path::new(&build_dir_str)).map_err(|e| {
337                    mlua::Error::RuntimeError(format!(
338                        "Failed to determine relative path for {:?}: {}",
339                        path, e
340                    ))
341                })?;
342                return Ok(Some(relative_path.to_string_lossy().to_string()));
343            }
344        }
345        Ok(None)
346    })?;
347    find_table.set("file", find_file_fn)?;
348
349    let utils_table: Table = lua.globals().get("UTILS")?;
350    utils_table.set("FIND", find_table)?;
351
352    Ok(())
353}