lux_lib/operations/
run.rs1use 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::{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 "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}
103
104impl<State> RunBuilder<'_, State>
105where
106 State: run_builder::State + run_builder::IsComplete,
107{
108 pub async fn run(self) -> Result<(), RunError> {
109 let run = self._build();
110 let workspace = run.workspace;
111 let config = run.config;
112 let extra_args = run.args;
113 let project = workspace.single_member_or_select(&run.package)?;
114 let toml = project.toml().into_local()?;
115
116 let run_spec = toml
117 .run()
118 .ok_or(RunError::NoRunField)?
119 .current_platform()
120 .clone();
121
122 let mut args: Vec<String> = run_spec.args.map(|args| args.into()).unwrap_or_default();
123
124 if !extra_args.is_empty() {
125 args.extend(extra_args.iter().cloned());
126 }
127 let disable_loader = run.disable_loader.unwrap_or(false);
128 match &run_spec.command {
129 Some(command) => {
130 run_with_command(workspace, command, run.dir, disable_loader, &args, config).await
131 }
132 None => run_with_local_lua(workspace, run.dir, disable_loader, &args, config).await,
133 }
134 }
135}
136
137async fn run_with_local_lua(
138 workspace: &Workspace,
139 root_dir: Option<PathBuf>,
140 disable_loader: bool,
141 args: &Vec<String>,
142 config: &Config,
143) -> Result<(), RunError> {
144 let version = workspace.lua_version(config)?;
145
146 let tree = workspace.tree(config)?;
147
148 let lua_cmd = LuaBinary::from(run_lua::resolve_lua_runtime(&version, config).await?);
149
150 RunLua::new()
151 .root(&root_dir.unwrap_or(workspace.root().to_path_buf()))
152 .tree(&tree)
153 .config(config)
154 .lua_cmd(lua_cmd)
155 .disable_loader(disable_loader)
156 .args(args)
157 .run_lua()
158 .await?;
159
160 Ok(())
161}
162
163async fn run_with_command(
164 workspace: &Workspace,
165 command: &RunCommand,
166 root_dir: Option<PathBuf>,
167 disable_loader: bool,
168 args: &[String],
169 config: &Config,
170) -> Result<(), RunError> {
171 let tree = workspace.tree(config)?;
172 let paths = Paths::new(&tree)?;
173
174 let lua_init = if tree.version().is_luau() || disable_loader {
175 None
176 } else if tree.version().lux_lib_dir().is_none() {
177 tracing::warn!("lux-lua library not found.\n Cannot use the `lux.loader`.\n To suppress this warning, set the `--no-loader` option.");
178 None
179 } else {
180 Some(paths.init())
181 };
182
183 let mut cmd = Command::new(command.deref());
184 if let Some(dir) = root_dir {
185 cmd.current_dir(dir);
186 } else {
187 cmd.current_dir(workspace.root());
188 }
189 match cmd
190 .args(args.iter().cloned().collect_vec())
191 .env("PATH", paths.path_prepended().joined())
192 .env("LUA_INIT", lua_init.unwrap_or_default())
193 .env("LUA_PATH", paths.package_path().joined())
194 .env("LUA_CPATH", paths.package_cpath().joined())
195 .status()
196 .await?
197 .code()
198 {
199 Some(0) => Ok(()),
200 code => Err(RunLuaError::LuaCommandNonZeroExitCode {
201 lua_cmd: command.to_string(),
202 exit_code: code,
203 }
204 .into()),
205 }
206}