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,
11    lua_installation::{LuaBinary, LuaInstallation},
12    lua_version::LuaVersion,
13    operations,
14    progress::MultiProgress,
15    project::Project,
16    rockspec::LuaVersionCompatibility,
17};
18
19use crate::build::{self, Build};
20
21#[derive(Args, Default)]
22#[clap(disable_help_flag = true)]
23pub struct RunLua {
24    /// Arguments to pass to Lua. See `lua -h`.
25    args: Option<Vec<String>>,
26
27    /// Path to the Lua interpreter to use.
28    #[arg(long)]
29    lua: Option<String>,
30
31    /// Add test dependencies to the environment.
32    #[arg(long)]
33    test: bool,
34
35    /// Add build dependencies to the environment.
36    #[arg(long)]
37    build: bool,
38
39    /// Disable the Lux loader.
40    /// If a rock has conflicting transitive dependencies,
41    /// disabling the Lux loader may result in the wrong modules being loaded.
42    #[clap(default_value_t = false)]
43    #[arg(long)]
44    no_loader: bool,
45
46    #[clap(flatten)]
47    build_args: Build,
48
49    /// Print help
50    #[arg(long)]
51    help: bool,
52}
53
54pub async fn run_lua(run_lua: RunLua, config: Config) -> Result<()> {
55    let project = Project::current()?;
56    let (lua_version, root, tree, mut welcome_message) = match &project {
57        Some(project) => (
58            project.toml().lua_version_matches(&config)?,
59            project.root().to_path_buf(),
60            project.tree(&config)?,
61            format!(
62                "Welcome to the lux Lua repl for {}.",
63                project.toml().package()
64            ),
65        ),
66        None => {
67            let version = LuaVersion::from(&config)?.clone();
68            (
69                version.clone(),
70                std::env::current_dir()?,
71                config.user_tree(version)?,
72                "Welcome to the lux Lua repl.".into(),
73            )
74        }
75    };
76
77    welcome_message = format!(
78        r#"{welcome_message}
79Run `lx lua --help` for options.
80To exit type 'exit()' or <C-d>.
81"#,
82    );
83
84    let lua_cmd = match run_lua.lua.map(LuaBinary::Custom) {
85        Some(lua_cmd) => lua_cmd,
86        None => {
87            let progress = MultiProgress::new_arc(&config);
88            let bar = progress.map(|progress| progress.new_bar());
89            let lua_cmd = LuaInstallation::new(&lua_version, &config, &bar)
90                .await?
91                .bin()
92                .as_ref()
93                .map(|lua_bin| LuaBinary::Custom(lua_bin.to_slash_lossy().to_string()))
94                .unwrap_or_else(|| LuaBinary::new(lua_version, &config));
95            bar.map(|bar| bar.finish_and_clear());
96            lua_cmd
97        }
98    };
99
100    if run_lua.help {
101        return print_lua_help(&lua_cmd).await;
102    }
103
104    if project.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 = match 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    {
135        Ok(output) => Ok(output),
136        Err(err) => Err(eyre!("Failed to run {}: {}", lua_cmd, err)),
137    }?;
138    let lua_help = String::from_utf8_lossy(&output.stderr)
139        .lines()
140        .skip(2)
141        .map(|line| format!("  {line}"))
142        .collect_vec()
143        .join("\n");
144    print!(
145        "
146Usage: lx lua -- [LUA_OPTIONS] [SCRIPT [ARGS]]...
147
148Arguments:
149  [LUA_OPTIONS]...
150{lua_help}
151
152Options:
153  --lua       Path to the Lua interpreter to use
154  -h, --help  Print help
155
156Build options (if running a repl for a project):
157  --test      Prepend test dependencies to the LUA_PATH and LUA_CPATH
158  --build     Prepend build dependencies to the LUA_PATH and LUA_CPATH
159  --no-lock   Ignore the project's lockfile and don't create one
160  --only-deps Build only the dependencies
161",
162    );
163    Ok(())
164}
165
166#[cfg(test)]
167mod test {
168    use std::path::PathBuf;
169
170    use lux_lib::config::ConfigBuilder;
171    use serial_test::serial;
172
173    use super::*;
174
175    #[serial]
176    #[tokio::test]
177    async fn test_run_lua() {
178        let args = RunLua {
179            args: Some(vec!["-v".into()]),
180            ..RunLua::default()
181        };
182        let temp: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
183        let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
184        let cwd = &std::env::current_dir().unwrap();
185        tokio::fs::create_dir_all(&temp).await.unwrap();
186        std::env::set_current_dir(&temp).unwrap();
187        let config = ConfigBuilder::new()
188            .unwrap()
189            .user_tree(Some(temp.clone()))
190            .data_dir(Some(data_dir))
191            .build()
192            .unwrap();
193        run_lua(args, config).await.unwrap();
194        std::env::set_current_dir(cwd).unwrap();
195    }
196}