lux_lib/operations/
run.rs1use 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 "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 eprintln!(
172 "⚠️ WARNING: lux-lua library not found.
173 Cannot use the `lux.loader`.
174 To suppress this warning, set the `--no-loader` option.
175 "
176 );
177 None
178 } else {
179 Some(paths.init())
180 };
181
182 let mut cmd = Command::new(command.deref());
183 if let Some(dir) = root_dir {
184 cmd.current_dir(dir);
185 } else {
186 cmd.current_dir(workspace.root());
187 }
188 match cmd
189 .args(args.into_iter().cloned().collect_vec())
190 .env("PATH", paths.path_prepended().joined())
191 .env("LUA_INIT", lua_init.unwrap_or_default())
192 .env("LUA_PATH", paths.package_path().joined())
193 .env("LUA_CPATH", paths.package_cpath().joined())
194 .status()
195 .await?
196 .code()
197 {
198 Some(0) => Ok(()),
199 code => Err(RunLuaError::LuaCommandNonZeroExitCode {
200 lua_cmd: command.to_string(),
201 exit_code: code,
202 }
203 .into()),
204 }
205}