lux_lib/operations/
run.rs1use std::ops::Deref;
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 args: &'a [String],
81 config: &'a Config,
82 disable_loader: Option<bool>,
83}
84
85impl<State> RunBuilder<'_, State>
86where
87 State: run_builder::State + run_builder::IsComplete,
88{
89 pub async fn run(self) -> Result<(), RunError> {
90 let run = self._build();
91 let project = run.project;
92 let config = run.config;
93 let extra_args = run.args;
94 let toml = project.toml().into_local()?;
95
96 let run_spec = toml
97 .run()
98 .ok_or(RunError::NoRunField)?
99 .current_platform()
100 .clone();
101
102 let mut args = run_spec.args.unwrap_or_default();
103
104 if !extra_args.is_empty() {
105 args.extend(extra_args.iter().cloned());
106 }
107 let disable_loader = run.disable_loader.unwrap_or(false);
108 match &run_spec.command {
109 Some(command) => {
110 run_with_command(project, command, disable_loader, &args, config).await
111 }
112 None => run_with_local_lua(project, disable_loader, &args, config).await,
113 }
114 }
115}
116
117async fn run_with_local_lua(
118 project: &Project,
119 disable_loader: bool,
120 args: &NonEmpty<String>,
121 config: &Config,
122) -> Result<(), RunError> {
123 let version = project.lua_version(config)?;
124
125 let tree = project.tree(config)?;
126 let args = &args.into_iter().cloned().collect();
127
128 RunLua::new()
129 .root(project.root())
130 .tree(&tree)
131 .config(config)
132 .lua_cmd(LuaBinary::new(version, config))
133 .disable_loader(disable_loader)
134 .args(args)
135 .run_lua()
136 .await?;
137
138 Ok(())
139}
140
141async fn run_with_command(
142 project: &Project,
143 command: &RunCommand,
144 disable_loader: bool,
145 args: &NonEmpty<String>,
146 config: &Config,
147) -> Result<(), RunError> {
148 let tree = project.tree(config)?;
149 let paths = Paths::new(&tree)?;
150
151 let lua_init = if disable_loader {
152 None
153 } else if tree.version().lux_lib_dir().is_none() {
154 eprintln!(
155 "⚠️ WARNING: lux-lua library not found.
156 Cannot use the `lux.loader`.
157 To suppress this warning, set the `--no-loader` option.
158 "
159 );
160 None
161 } else {
162 Some(paths.init())
163 };
164
165 match Command::new(command.deref())
166 .args(args.into_iter().cloned().collect_vec())
167 .current_dir(project.root().deref())
168 .env("PATH", paths.path_prepended().joined())
169 .env("LUA_INIT", lua_init.unwrap_or_default())
170 .env("LUA_PATH", paths.package_path().joined())
171 .env("LUA_CPATH", paths.package_cpath().joined())
172 .status()
173 .await?
174 .code()
175 {
176 Some(0) => Ok(()),
177 code => Err(RunLuaError::LuaCommandNonZeroExitCode {
178 lua_cmd: command.to_string(),
179 exit_code: code,
180 }
181 .into()),
182 }
183}