Skip to main content

lux_cli/
run_lua.rs

1use std::path::PathBuf;
2
3use path_slash::PathExt;
4use tokio::process::Command;
5
6use clap::Args;
7use itertools::Itertools;
8use lux_lib::{
9    config::Config,
10    lua_installation::{LuaBinary, LuaInstallation},
11    lua_version::LuaVersion,
12    operations,
13    workspace::Workspace,
14};
15
16use miette::{IntoDiagnostic, Result, WrapErr};
17
18use crate::build::{self, Build};
19
20#[derive(Args, Default)]
21#[clap(disable_help_flag = true)]
22pub struct RunLua {
23    /// Arguments to pass to Lua. See `lua -h`.
24    args: Option<Vec<String>>,
25
26    /// Path to the Lua interpreter to use.
27    #[arg(long)]
28    lua: Option<String>,
29
30    /// Add test dependencies to the environment.
31    #[arg(long)]
32    test: bool,
33
34    /// Add build dependencies to the environment.
35    #[arg(long)]
36    build: bool,
37
38    /// Disable the Lux loader.
39    /// If a rock has conflicting transitive dependencies,
40    /// disabling the Lux loader may result in the wrong modules being loaded.
41    #[clap(default_value_t = false)]
42    #[arg(long)]
43    no_loader: bool,
44
45    #[clap(flatten)]
46    build_args: Build,
47
48    /// Print help
49    #[arg(long)]
50    help: bool,
51}
52
53pub async fn run_lua(run_lua: RunLua, config: Config) -> Result<()> {
54    let workspace = Workspace::current()?;
55    let (lua_version, root, tree, mut welcome_message) = match &workspace {
56        Some(workspace) => {
57            let package_names = workspace
58                .members()
59                .iter()
60                .map(|project| project.toml().package())
61                .join(",");
62            (
63                workspace.lua_version(&config)?,
64                workspace.root().to_path_buf(),
65                workspace.tree(&config)?,
66                format!("Welcome to the lux Lua repl for {package_names}.",),
67            )
68        }
69        None => {
70            let version = LuaVersion::from(&config)?.clone();
71            (
72                version.clone(),
73                std::env::current_dir().into_diagnostic()?,
74                config.user_tree(version)?,
75                "Welcome to the lux Lua repl.".into(),
76            )
77        }
78    };
79
80    welcome_message = format!(
81        r#"{welcome_message}
82Run `lx lua --help` for options.
83To exit type 'exit()' or <C-d>.
84"#,
85    );
86
87    let lua_cmd = match run_lua.lua.map(LuaBinary::Custom) {
88        Some(lua_cmd) => lua_cmd,
89        None => {
90            let lua_cmd = LuaInstallation::new(&lua_version, &config)
91                .await?
92                .bin()
93                .as_ref()
94                .map(|lua_bin| LuaBinary::Custom(lua_bin.to_slash_lossy().to_string()))
95                .unwrap_or_else(|| LuaBinary::new(lua_version, &config));
96            lua_cmd
97        }
98    };
99
100    if run_lua.help {
101        return print_lua_help(&lua_cmd).await;
102    }
103
104    if workspace.is_some() {
105        build::build(run_lua.build_args, config.clone()).await?;
106    }
107
108    let args = &run_lua.args.unwrap_or_default();
109
110    operations::RunLua::new()
111        .root(&root)
112        .tree(&tree)
113        .config(&config)
114        .lua_cmd(lua_cmd)
115        .args(args)
116        .prepend_test_paths(run_lua.test)
117        .prepend_build_paths(run_lua.build)
118        .disable_loader(run_lua.no_loader)
119        .lua_init("exit = os.exit".to_string())
120        .welcome_message(welcome_message)
121        .run_lua()
122        .await?;
123
124    Ok(())
125}
126
127async fn print_lua_help(lua_cmd: &LuaBinary) -> Result<()> {
128    let lua_cmd_path: PathBuf = lua_cmd.clone().try_into()?;
129    let output = Command::new(lua_cmd_path.to_string_lossy().to_string())
130        // HACK: This fails with exit 1, because lua doesn't actually have a help flag (╯°□°)╯︵ ┻━┻
131        .arg("-h")
132        .output()
133        .await
134        .into_diagnostic()
135        .wrap_err(format!("Failed to run {lua_cmd}"))?;
136    let lua_help = String::from_utf8_lossy(&output.stderr)
137        .lines()
138        .skip(2)
139        .map(|line| format!("  {line}"))
140        .collect_vec()
141        .join("\n");
142    print!(
143        "
144Usage: lx lua -- [LUA_OPTIONS] [SCRIPT [ARGS]]...
145
146Arguments:
147  [LUA_OPTIONS]...
148{lua_help}
149
150Options:
151  --lua       Path to the Lua interpreter to use
152  -h, --help  Print help
153
154Build options (if running a repl for a project):
155  --test      Prepend test dependencies to the LUA_PATH and LUA_CPATH
156  --build     Prepend build dependencies to the LUA_PATH and LUA_CPATH
157  --no-lock   Ignore the project's lockfile and don't create one
158  --only-deps Build only the dependencies
159",
160    );
161    Ok(())
162}
163
164#[cfg(test)]
165mod tests {
166    use std::path::PathBuf;
167
168    use lux_lib::config::ConfigBuilder;
169    use serial_test::serial;
170
171    use super::*;
172
173    #[serial]
174    #[tokio::test]
175    async fn test_run_lua() {
176        let args = RunLua {
177            args: Some(vec!["-v".into()]),
178            ..RunLua::default()
179        };
180        let temp: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
181        let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
182        let cwd = &std::env::current_dir().unwrap();
183        tokio::fs::create_dir_all(&temp).await.unwrap();
184        std::env::set_current_dir(&temp).unwrap();
185        let config = ConfigBuilder::new()
186            .unwrap()
187            .user_tree(Some(temp.clone()))
188            .data_dir(Some(data_dir))
189            .build()
190            .unwrap();
191        run_lua(args, config).await.unwrap();
192        std::env::set_current_dir(cwd).unwrap();
193    }
194}