Skip to main content

lux_lib/operations/
run.rs

1use std::{ops::Deref, path::PathBuf};
2
3use bon::Builder;
4use itertools::Itertools;
5use miette::Diagnostic;
6use serde::Deserialize;
7use thiserror::Error;
8use tokio::process::Command;
9
10use crate::{
11    config::Config,
12    lua_installation::{LuaBinary, LuaInstallationError},
13    lua_rockspec::LuaVersionError,
14    operations::run_lua::{self, RunLua},
15    package::PackageName,
16    path::{LuaEnvironment, Paths, PathsError},
17    project::project_toml::LocalProjectTomlValidationError,
18    tree::InstallTree,
19    workspace::{Workspace, WorkspaceError, WorkspaceTreeError},
20};
21
22use super::RunLuaError;
23
24#[derive(Debug, Error, Diagnostic)]
25#[error("'{0}' should not be used as a `command` as it is not cross-platform.
26You should only change the default `command` if it is a different Lua interpreter that behaves identically on all platforms.
27Consider removing the `command` field and letting Lux choose the default Lua interpreter instead.")]
28pub struct RunCommandError(String);
29
30#[derive(Debug, Clone)]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32#[cfg_attr(feature = "schema", serde(transparent))]
33pub struct RunCommand(String);
34
35impl RunCommand {
36    pub fn from(command: String) -> Result<Self, RunCommandError> {
37        match command.as_str() {
38            // Common Lua interpreters that could lead to cross-platform issues
39            // Luajit is also included because it may or may not have lua52 syntax support compiled in.
40            "lua" | "lua5.1" | "lua5.2" | "lua5.3" | "lua5.4" | "luajit" => {
41                Err(RunCommandError(command))
42            }
43            _ => Ok(Self(command)),
44        }
45    }
46}
47
48impl<'de> Deserialize<'de> for RunCommand {
49    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
50    where
51        D: serde::Deserializer<'de>,
52    {
53        let command = String::deserialize(deserializer)?;
54
55        RunCommand::from(command).map_err(serde::de::Error::custom)
56    }
57}
58
59impl Deref for RunCommand {
60    type Target = String;
61
62    fn deref(&self) -> &Self::Target {
63        &self.0
64    }
65}
66
67#[derive(Debug, Error, Diagnostic)]
68#[error(transparent)]
69#[non_exhaustive]
70pub enum RunError {
71    #[diagnostic(transparent)]
72    Toml(#[from] LocalProjectTomlValidationError),
73    #[diagnostic(transparent)]
74    RunCommand(#[from] RunCommandError),
75    #[diagnostic(transparent)]
76    LuaVersion(#[from] LuaVersionError),
77    #[diagnostic(transparent)]
78    RunLua(#[from] RunLuaError),
79    #[error("failed to detect or install a luau runtime")]
80    #[diagnostic(forward(0))]
81    LuaInstallation(#[from] LuaInstallationError),
82    #[diagnostic(transparent)]
83    WorkspaceError(#[from] WorkspaceError),
84    #[diagnostic(transparent)]
85    WorkspaceTree(#[from] WorkspaceTreeError),
86    Io(#[from] std::io::Error),
87    #[diagnostic(transparent)]
88    Paths(#[from] PathsError),
89    #[error("No `run` field found in `lux.toml`")]
90    NoRunField,
91}
92
93#[derive(Builder)]
94#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
95pub struct Run<'a> {
96    workspace: &'a Workspace,
97    package: Option<PackageName>,
98    dir: Option<PathBuf>,
99    args: &'a [String],
100    config: &'a Config,
101    disable_loader: Option<bool>,
102    environment: LuaEnvironment,
103}
104
105impl<State> RunBuilder<'_, State>
106where
107    State: run_builder::State + run_builder::IsComplete,
108{
109    pub async fn run(self) -> Result<(), RunError> {
110        let run = self._build();
111        let workspace = run.workspace;
112        let config = run.config;
113        let extra_args = run.args;
114        let project = workspace.single_member_or_select(&run.package)?;
115        let toml = project.toml().into_local()?;
116
117        let run_spec = toml
118            .run()
119            .ok_or(RunError::NoRunField)?
120            .current_platform()
121            .clone();
122
123        let mut args: Vec<String> = run_spec.args.map(|args| args.into()).unwrap_or_default();
124
125        if !extra_args.is_empty() {
126            args.extend(extra_args.iter().cloned());
127        }
128        let disable_loader = run.disable_loader.unwrap_or(false);
129        let environment = run.environment;
130        match &run_spec.command {
131            Some(command) => {
132                run_with_command(
133                    workspace,
134                    command,
135                    run.dir,
136                    disable_loader,
137                    &args,
138                    config,
139                    environment,
140                )
141                .await
142            }
143            None => {
144                run_with_local_lua(
145                    workspace,
146                    run.dir,
147                    disable_loader,
148                    &args,
149                    config,
150                    environment,
151                )
152                .await
153            }
154        }
155    }
156}
157
158async fn run_with_local_lua(
159    workspace: &Workspace,
160    root_dir: Option<PathBuf>,
161    disable_loader: bool,
162    args: &Vec<String>,
163    config: &Config,
164    environment: LuaEnvironment,
165) -> Result<(), RunError> {
166    let version = workspace.lua_version(config)?;
167
168    let tree = workspace.tree(config)?;
169
170    let lua_cmd = LuaBinary::from(run_lua::resolve_lua_runtime(&version, config).await?);
171
172    RunLua::new()
173        .root(&root_dir.unwrap_or(workspace.root().to_path_buf()))
174        .tree(&tree)
175        .config(config)
176        .lua_cmd(lua_cmd)
177        .disable_loader(disable_loader)
178        .args(args)
179        .environment(environment)
180        .run_lua()
181        .await?;
182
183    Ok(())
184}
185
186async fn run_with_command(
187    workspace: &Workspace,
188    command: &RunCommand,
189    root_dir: Option<PathBuf>,
190    disable_loader: bool,
191    args: &[String],
192    config: &Config,
193    environment: LuaEnvironment,
194) -> Result<(), RunError> {
195    let tree = workspace.tree(config)?;
196    let paths = Paths::for_environment(&tree, config, environment)?;
197
198    let lua_init = if tree.version().is_luau() || disable_loader {
199        None
200    } else if tree.version().lux_lib_dir().is_none() {
201        tracing::warn!("lux-lua library not found.\n    Cannot use the `lux.loader`.\n    To suppress this warning, set the `--no-loader` option.");
202        None
203    } else {
204        Some(paths.init())
205    };
206
207    let mut cmd = Command::new(command.deref());
208    if let Some(dir) = root_dir {
209        cmd.current_dir(dir);
210    } else {
211        cmd.current_dir(workspace.root());
212    }
213    match cmd
214        .args(args.iter().cloned().collect_vec())
215        .env("PATH", paths.path_prepended().joined())
216        .envs(paths.lua_env_vars(environment, &lua_init.unwrap_or_default()))
217        .status()
218        .await?
219        .code()
220    {
221        Some(0) => Ok(()),
222        code => Err(RunLuaError::LuaCommandNonZeroExitCode {
223            lua_cmd: command.to_string(),
224            exit_code: code,
225        }
226        .into()),
227    }
228}