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 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 "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}