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