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