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