Skip to main content

lux_lib/operations/
run.rs

1use std::{ops::Deref, path::PathBuf};
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    dir: Option<PathBuf>,
81    args: &'a [String],
82    config: &'a Config,
83    disable_loader: Option<bool>,
84}
85
86impl<State> RunBuilder<'_, State>
87where
88    State: run_builder::State + run_builder::IsComplete,
89{
90    pub async fn run(self) -> Result<(), RunError> {
91        let run = self._build();
92        let project = run.project;
93        let config = run.config;
94        let extra_args = run.args;
95        let toml = project.toml().into_local()?;
96
97        let run_spec = toml
98            .run()
99            .ok_or(RunError::NoRunField)?
100            .current_platform()
101            .clone();
102
103        let mut args = run_spec.args.unwrap_or_default();
104
105        if !extra_args.is_empty() {
106            args.extend(extra_args.iter().cloned());
107        }
108        let disable_loader = run.disable_loader.unwrap_or(false);
109        match &run_spec.command {
110            Some(command) => {
111                run_with_command(project, command, run.dir, disable_loader, &args, config).await
112            }
113            None => run_with_local_lua(project, run.dir, disable_loader, &args, config).await,
114        }
115    }
116}
117
118async fn run_with_local_lua(
119    project: &Project,
120    root_dir: Option<PathBuf>,
121    disable_loader: bool,
122    args: &NonEmpty<String>,
123    config: &Config,
124) -> Result<(), RunError> {
125    let version = project.lua_version(config)?;
126
127    let tree = project.tree(config)?;
128    let args = &args.into_iter().cloned().collect();
129
130    RunLua::new()
131        .root(&root_dir.unwrap_or(project.root().to_path_buf()))
132        .tree(&tree)
133        .config(config)
134        .lua_cmd(LuaBinary::new(version, config))
135        .disable_loader(disable_loader)
136        .args(args)
137        .run_lua()
138        .await?;
139
140    Ok(())
141}
142
143async fn run_with_command(
144    project: &Project,
145    command: &RunCommand,
146    root_dir: Option<PathBuf>,
147    disable_loader: bool,
148    args: &NonEmpty<String>,
149    config: &Config,
150) -> Result<(), RunError> {
151    let tree = project.tree(config)?;
152    let paths = Paths::new(&tree)?;
153
154    let lua_init = if disable_loader {
155        None
156    } else if tree.version().lux_lib_dir().is_none() {
157        eprintln!(
158            "⚠️ WARNING: lux-lua library not found.
159    Cannot use the `lux.loader`.
160    To suppress this warning, set the `--no-loader` option.
161                    "
162        );
163        None
164    } else {
165        Some(paths.init())
166    };
167
168    let mut cmd = Command::new(command.deref());
169    if let Some(dir) = root_dir {
170        cmd.current_dir(dir);
171    } else {
172        cmd.current_dir(project.root());
173    }
174    match cmd
175        .args(args.into_iter().cloned().collect_vec())
176        .env("PATH", paths.path_prepended().joined())
177        .env("LUA_INIT", lua_init.unwrap_or_default())
178        .env("LUA_PATH", paths.package_path().joined())
179        .env("LUA_CPATH", paths.package_cpath().joined())
180        .status()
181        .await?
182        .code()
183    {
184        Some(0) => Ok(()),
185        code => Err(RunLuaError::LuaCommandNonZeroExitCode {
186            lua_cmd: command.to_string(),
187            exit_code: code,
188        }
189        .into()),
190    }
191}