lux_lib/operations/
run.rs

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