lux_lib/operations/
run.rs1use 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 "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}