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