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 eyre::{eyre, Result};
8use itertools::Itertools;
9use lux_lib::{
10    config::{Config, LuaVersion},
11    lua_installation::{LuaBinary, LuaInstallation},
12    operations,
13    progress::MultiProgress,
14    project::Project,
15    rockspec::LuaVersionCompatibility,
16};
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 project = Project::current()?;
55    let (lua_version, root, tree, mut welcome_message) = match &project {
56        Some(project) => (
57            project.toml().lua_version_matches(&config)?,
58            project.root().to_path_buf(),
59            project.tree(&config)?,
60            format!(
61                "Welcome to the lux Lua repl for {}.",
62                project.toml().package()
63            ),
64        ),
65        None => {
66            let version = LuaVersion::from(&config)?.clone();
67            (
68                version.clone(),
69                std::env::current_dir()?,
70                config.user_tree(version)?,
71                "Welcome to the lux Lua repl.".into(),
72            )
73        }
74    };
75
76    welcome_message = format!(
77        r#"{welcome_message}
78Run `lx lua --help` for options.
79To exit type 'exit()' or <C-d>.
80"#,
81    );
82
83    let lua_cmd = match run_lua.lua.map(LuaBinary::Custom) {
84        Some(lua_cmd) => lua_cmd,
85        None => {
86            let progress = MultiProgress::new_arc(&config);
87            let bar = progress.map(|progress| progress.new_bar());
88            let lua_cmd = LuaInstallation::new(&lua_version, &config, &bar)
89                .await?
90                .bin()
91                .as_ref()
92                .map(|lua_bin| LuaBinary::Custom(lua_bin.to_slash_lossy().to_string()))
93                .unwrap_or_else(|| LuaBinary::new(lua_version, &config));
94            bar.map(|bar| bar.finish_and_clear());
95            lua_cmd
96        }
97    };
98
99    if run_lua.help {
100        return print_lua_help(&lua_cmd).await;
101    }
102
103    if project.is_some() {
104        build::build(run_lua.build_args, config.clone()).await?;
105    }
106
107    let args = &run_lua.args.unwrap_or_default();
108
109    operations::RunLua::new()
110        .root(&root)
111        .tree(&tree)
112        .config(&config)
113        .lua_cmd(lua_cmd)
114        .args(args)
115        .prepend_test_paths(run_lua.test)
116        .prepend_build_paths(run_lua.build)
117        .disable_loader(run_lua.no_loader)
118        .lua_init("exit = os.exit".to_string())
119        .welcome_message(welcome_message)
120        .run_lua()
121        .await?;
122
123    Ok(())
124}
125
126async fn print_lua_help(lua_cmd: &LuaBinary) -> Result<()> {
127    let lua_cmd_path: PathBuf = lua_cmd.clone().try_into()?;
128    let output = match Command::new(lua_cmd_path.to_string_lossy().to_string())
129        // HACK: This fails with exit 1, because lua doesn't actually have a help flag (╯°□°)╯︵ ┻━┻
130        .arg("-h")
131        .output()
132        .await
133    {
134        Ok(output) => Ok(output),
135        Err(err) => Err(eyre!("Failed to run {}: {}", lua_cmd, err)),
136    }?;
137    let lua_help = String::from_utf8_lossy(&output.stderr)
138        .lines()
139        .skip(2)
140        .map(|line| format!("  {line}"))
141        .collect_vec()
142        .join("\n");
143    print!(
144        "
145Usage: lx lua -- [LUA_OPTIONS] [SCRIPT [ARGS]]...
146
147Arguments:
148  [LUA_OPTIONS]...
149{lua_help}
150
151Options:
152  --lua       Path to the Lua interpreter to use
153  -h, --help  Print help
154
155Build options (if running a repl for a project):
156  --test      Prepend test dependencies to the LUA_PATH and LUA_CPATH
157  --build     Prepend build dependencies to the LUA_PATH and LUA_CPATH
158  --no-lock   Ignore the project's lockfile and don't create one
159  --only-deps Build only the dependencies
160",
161    );
162    Ok(())
163}
164
165#[cfg(test)]
166mod test {
167    use std::path::PathBuf;
168
169    use lux_lib::config::ConfigBuilder;
170    use serial_test::serial;
171
172    use super::*;
173
174    #[serial]
175    #[tokio::test]
176    async fn test_run_lua() {
177        let args = RunLua {
178            args: Some(vec!["-v".into()]),
179            ..RunLua::default()
180        };
181        let temp: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
182        let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
183        let cwd = &std::env::current_dir().unwrap();
184        tokio::fs::create_dir_all(&temp).await.unwrap();
185        std::env::set_current_dir(&temp).unwrap();
186        let config = ConfigBuilder::new()
187            .unwrap()
188            .user_tree(Some(temp.clone()))
189            .data_dir(Some(data_dir))
190            .build()
191            .unwrap();
192        run_lua(args, config).await.unwrap();
193        std::env::set_current_dir(cwd).unwrap();
194    }
195}