lux_cli/
run_lua.rs

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