lux_lib/operations/
run.rs

1use std::ops::Deref;
2
3use itertools::Itertools;
4use nonempty::NonEmpty;
5use serde::Deserialize;
6use thiserror::Error;
7use tokio::process::Command;
8
9use crate::{
10    config::Config,
11    lua_installation::LuaBinary,
12    lua_rockspec::LuaVersionError,
13    operations::run_lua::RunLua,
14    path::{Paths, PathsError},
15    project::{project_toml::LocalProjectTomlValidationError, Project, ProjectTreeError},
16};
17
18use super::RunLuaError;
19
20#[derive(Debug, Error)]
21#[error("`{0}` should not be used as a `command` as it is not cross-platform.
22You should only change the default `command` if it is a different Lua interpreter that behaves identically on all platforms.
23Consider removing the `command` field and letting Lux choose the default Lua interpreter instead.")]
24pub struct RunCommandError(String);
25
26#[derive(Debug, Clone)]
27pub struct RunCommand(String);
28
29impl RunCommand {
30    pub fn from(command: String) -> Result<Self, RunCommandError> {
31        match command.as_str() {
32            // Common Lua interpreters that could lead to cross-platform issues
33            // Luajit is also included because it may or may not have lua52 syntax support compiled in.
34            "lua" | "lua5.1" | "lua5.2" | "lua5.3" | "lua5.4" | "luajit" => {
35                Err(RunCommandError(command))
36            }
37            _ => Ok(Self(command)),
38        }
39    }
40}
41
42impl<'de> Deserialize<'de> for RunCommand {
43    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
44    where
45        D: serde::Deserializer<'de>,
46    {
47        let command = String::deserialize(deserializer)?;
48
49        RunCommand::from(command).map_err(serde::de::Error::custom)
50    }
51}
52
53impl Deref for RunCommand {
54    type Target = String;
55
56    fn deref(&self) -> &Self::Target {
57        &self.0
58    }
59}
60
61#[derive(Debug, Error)]
62#[error(transparent)]
63pub enum RunError {
64    Toml(#[from] LocalProjectTomlValidationError),
65    RunCommand(#[from] RunCommandError),
66    LuaVersion(#[from] LuaVersionError),
67    RunLua(#[from] RunLuaError),
68    ProjectTree(#[from] ProjectTreeError),
69    Io(#[from] std::io::Error),
70    Paths(#[from] PathsError),
71    #[error("No `run` field found in `lux.toml`")]
72    NoRunField,
73}
74
75async fn run_with_local_lua(
76    project: &Project,
77    args: &NonEmpty<String>,
78    config: &Config,
79) -> Result<(), RunError> {
80    let version = project.lua_version(config)?;
81
82    let tree = project.tree(config)?;
83    let args = &args.into_iter().cloned().collect();
84
85    RunLua::new()
86        .root(project.root())
87        .tree(&tree)
88        .config(config)
89        .lua_cmd(LuaBinary::new(version, config))
90        .args(args)
91        .run_lua()
92        .await?;
93
94    Ok(())
95}
96
97async fn run_with_command(
98    project: &Project,
99    command: &RunCommand,
100    args: &NonEmpty<String>,
101    config: &Config,
102) -> Result<(), RunError> {
103    let tree = project.tree(config)?;
104    let paths = Paths::new(&tree)?;
105
106    match Command::new(command.deref())
107        .args(args.into_iter().cloned().collect_vec())
108        .current_dir(project.root().deref())
109        .env("PATH", paths.path_prepended().joined())
110        .env("LUA_PATH", paths.package_path().joined())
111        .env("LUA_CPATH", paths.package_cpath().joined())
112        .status()
113        .await?
114        .code()
115    {
116        Some(0) => Ok(()),
117        code => Err(RunLuaError::LuaCommandNonZeroExitCode {
118            lua_cmd: command.to_string(),
119            exit_code: code,
120        }
121        .into()),
122    }
123}
124
125pub async fn run(
126    project: &Project,
127    extra_args: &[String],
128    config: &Config,
129) -> Result<(), RunError> {
130    let toml = project.toml().into_local()?;
131
132    let run_spec = toml
133        .run()
134        .ok_or(RunError::NoRunField)?
135        .current_platform()
136        .clone();
137
138    let mut args = run_spec.args.unwrap_or_default();
139
140    if !extra_args.is_empty() {
141        args.extend(extra_args.iter().cloned());
142    }
143
144    match &run_spec.command {
145        Some(command) => run_with_command(project, command, &args, config).await,
146        None => run_with_local_lua(project, &args, config).await,
147    }
148}