sile/
lib.rs

1// rust-embed include attributes have issues with lots of matches...
2#![recursion_limit = "2048"]
3
4use mlua::prelude::*;
5
6#[cfg(not(feature = "static"))]
7use mlua::chunk;
8use std::env;
9use std::path::PathBuf;
10
11#[cfg(feature = "cli")]
12pub mod cli;
13
14#[cfg(feature = "static")]
15pub mod embed;
16
17pub mod types;
18
19pub type Result<T> = anyhow::Result<T>;
20
21pub fn start_luavm() -> crate::Result<Lua> {
22    let mut lua = unsafe { Lua::unsafe_new() };
23    #[cfg(feature = "static")]
24    {
25        lua = embed::inject_embedded_loaders(lua)?;
26    }
27    // For parity with the legacy Lua arg parser, allows inspection of CLI args at runtime in Lua.
28    {
29        let rt = std::env::args()
30            .next()
31            .unwrap_or_else(|| "sile".to_string());
32        let args: Vec<String> = std::env::args().skip(1).collect();
33        let arg_table = lua.create_table()?;
34        for (i, arg) in args.iter().enumerate() {
35            arg_table.set(i + 1, arg.clone())?;
36        }
37        // A bit non-orthodox, but the Lua side sets the VM chunk name to what moste CLIs expect $0
38        // to be, their own binary name. The Rust side of mlua is setting the chunk name to =[C],
39        // making error messages a bit cryptic. By setting a 0 index here we give the later Lua
40        // side a chance to replace the chunk name with it.
41        arg_table.set(0, rt)?;
42        lua.globals().set("arg", arg_table)?;
43    }
44    lua = inject_paths(lua)?;
45    lua = load_sile(lua)?;
46    lua = inject_version(lua)?;
47    Ok(lua)
48}
49
50pub fn inject_paths(lua: Lua) -> crate::Result<Lua> {
51    // Note set_name() here can't be left blank or it will resolve to src/lib.rs, and it can't be
52    // something custom that doesn't resolve to an actual file because it will turn up in the
53    // makedepends list. We use the internal Lua VM's own =[C] syntax which will be relpaced with
54    // $0 so that the Rust binary becomes the listed dependency.
55    #[cfg(feature = "static")]
56    lua.load(r#"require("core.pathsetup")"#)
57        .set_name("=[C]")
58        .exec()?;
59    #[cfg(not(feature = "static"))]
60    {
61        let datadir = env!("CONFIGURE_DATADIR").to_string();
62        let sile_path = match env::var("SILE_PATH") {
63            Ok(val) => format!("{datadir};{val}"),
64            Err(_) => datadir,
65        };
66        let sile_path: LuaString = lua.create_string(&sile_path)?;
67        lua.load(chunk! {
68            local status
69            for path in string.gmatch($sile_path, "[^;]+") do
70                status = pcall(dofile, path .. "/core/pathsetup.lua")
71                if status then break end
72            end
73            if not status then
74                dofile("./core/pathsetup.lua")
75            end
76        })
77        .set_name("=[C]")
78        .exec()?;
79    }
80    Ok(lua)
81}
82
83pub fn get_rusile_exports(lua: &Lua) -> LuaResult<LuaTable> {
84    let exports = lua.create_table()?;
85    exports.set("semver", LuaFunction::wrap_raw(types::semver::semver))?;
86    exports.set("setenv", LuaFunction::wrap_raw(setenv))?;
87    Ok(exports)
88}
89
90fn setenv(key: String, value: String) {
91    env::set_var(key, value);
92}
93
94pub fn inject_version(lua: Lua) -> crate::Result<Lua> {
95    let sile: LuaTable = lua.globals().get("SILE")?;
96    let mut full_version: String = sile.get("full_version")?;
97    full_version.push_str(" [Rust]");
98    sile.set("full_version", full_version)?;
99    Ok(lua)
100}
101
102pub fn load_sile(lua: Lua) -> crate::Result<Lua> {
103    let entry: LuaString = lua.create_string("core.sile")?;
104    let require: LuaFunction = lua.globals().get("require")?;
105    require.call::<LuaTable>(entry)?;
106    Ok(lua)
107}
108
109pub fn version() -> crate::Result<String> {
110    let lua = start_luavm()?;
111    let sile: LuaTable = lua.globals().get("SILE")?;
112    let full_version: String = sile.get("full_version")?;
113    Ok(full_version)
114}
115
116// Yes I know this should be taking a struct, probably 1 with what ends up being SILE.input and one
117// with other stuff the CLI may inject, but I'm playing with what a minimum/maximum set of
118// parameters would look like here while maintaining compatiblitiy with the Lua CLI.
119#[allow(clippy::too_many_arguments)]
120pub fn run(
121    inputs: Option<Vec<PathBuf>>,
122    backend: Option<String>,
123    class: Option<String>,
124    debugs: Option<Vec<String>>,
125    evaluates: Option<Vec<String>>,
126    evaluate_afters: Option<Vec<String>>,
127    fontmanager: Option<String>,
128    luarocks_tree: Option<Vec<PathBuf>>,
129    makedeps: Option<PathBuf>,
130    output: Option<PathBuf>,
131    options: Option<Vec<String>>,
132    preambles: Option<Vec<PathBuf>>,
133    postambles: Option<Vec<PathBuf>>,
134    uses: Option<Vec<String>>,
135    quiet: bool,
136    traceback: bool,
137) -> crate::Result<()> {
138    let lua = start_luavm()?;
139    let sile: LuaTable = lua.globals().get("SILE")?;
140    sile.set("traceback", traceback)?;
141    sile.set("quiet", quiet)?;
142    let mut has_input_filename = false;
143    if let Some(flags) = debugs {
144        let debug_flags: LuaTable = sile.get("debugFlags")?;
145        for flag in flags {
146            debug_flags.set(flag, true)?;
147        }
148    }
149    let full_version: String = sile.get("full_version")?;
150    let sile_input: LuaTable = sile.get("input")?;
151    if let Some(expressions) = evaluates {
152        sile_input.set("evaluates", expressions)?;
153    }
154    if let Some(expressions) = evaluate_afters {
155        sile_input.set("evaluateAfters", expressions)?;
156    }
157    if let Some(backend) = backend {
158        sile_input.set("backend", backend)?;
159    }
160    if let Some(fontmanager) = fontmanager {
161        sile_input.set("fontmanager", fontmanager)?;
162    }
163    if let Some(trees) = luarocks_tree {
164        sile_input.set("luarocksTrees", trees)?;
165    }
166    if let Some(class) = class {
167        sile_input.set("class", class)?;
168    }
169    if let Some(paths) = preambles {
170        sile_input.set("preambles", paths_to_strings(paths))?;
171    }
172    if let Some(paths) = postambles {
173        sile_input.set("postambles", paths_to_strings(paths))?;
174    }
175    if let Some(path) = makedeps {
176        sile_input.set("makedeps", path_to_string(&path))?;
177    }
178    if let Some(path) = output {
179        sile.set("outputFilename", path_to_string(&path))?;
180        has_input_filename = true;
181    }
182    if let Some(options) = options {
183        let parameters: LuaAnyUserData = sile.get::<LuaTable>("parserBits")?.get("parameters")?;
184        let input_options: LuaTable = sile_input.get("options")?;
185        for option in options.iter() {
186            let parameters: LuaTable = parameters
187                .call_method("match", lua.create_string(option)?)
188                .context("failed to call `parameters:match()`")?;
189            for parameter in parameters.pairs::<LuaValue, LuaValue>() {
190                let (key, value) = parameter?;
191                let _ = input_options.set(key, value);
192            }
193        }
194    }
195    if let Some(modules) = uses {
196        let cliuse: LuaAnyUserData = sile.get::<LuaTable>("parserBits")?.get("cliuse")?;
197        let input_uses: LuaTable = sile_input.get("uses")?;
198        for module in modules.iter() {
199            let module = lua.create_string(module)?;
200            let spec: LuaTable = cliuse
201                .call_method::<_>("match", module)
202                .context("failed to call `cliuse:match()`")?;
203            let _ = input_uses.push(spec);
204        }
205    }
206    if !quiet {
207        eprintln!("{full_version}");
208    }
209    let init: LuaFunction = sile.get("init")?;
210    init.call::<LuaValue>(())?;
211    if let Some(inputs) = inputs {
212        let input_filenames: LuaTable = lua.create_table()?;
213        for input in inputs.iter() {
214            let path = &path_to_string(input);
215            if !has_input_filename && path != "-" {
216                has_input_filename = true;
217            }
218            input_filenames.push(lua.create_string(path)?)?;
219        }
220        if !has_input_filename {
221            panic!(
222                "\nUnable to derive an output filename (perhaps because input is a STDIO stream)\nPlease use --output to set one explicitly."
223            );
224        }
225        sile_input.set("filenames", input_filenames)?;
226        let input_uses: LuaTable = sile_input.get("uses")?;
227        let r#use: LuaFunction = sile.get("use")?;
228        for spec in input_uses.sequence_values::<LuaTable>() {
229            let spec = spec?;
230            let module: LuaString = spec.get("module")?;
231            let options: LuaTable = spec.get("options")?;
232            r#use.call::<LuaValue>((module, options))?;
233        }
234        let input_filenames: LuaTable = sile_input.get("filenames")?;
235        let process_file: LuaFunction = sile.get("processFile")?;
236        for file in input_filenames.sequence_values::<LuaString>() {
237            process_file.call::<LuaValue>(file?)?;
238        }
239        let finish: LuaFunction = sile.get("finish")?;
240        finish.call::<LuaValue>(())?;
241    } else {
242        let repl_module: LuaString = lua.create_string("core.repl")?;
243        let require: LuaFunction = lua.globals().get("require")?;
244        let repl: LuaTable = require.call::<LuaTable>(repl_module)?;
245        repl.call_method::<LuaValue>("enter", ())?;
246    }
247    Ok(())
248}
249
250fn path_to_string(path: &PathBuf) -> String {
251    path.clone().into_os_string().into_string().unwrap()
252}
253
254fn paths_to_strings(paths: Vec<PathBuf>) -> Vec<String> {
255    paths.iter().map(path_to_string).collect()
256}