lux_lib/operations/
run.rs1use std::{ops::Deref, path::PathBuf};
2
3use bon::Builder;
4use itertools::Itertools;
5use miette::Diagnostic;
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, Diagnostic)]
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, Diagnostic)]
66#[error(transparent)]
67pub enum RunError {
68 #[diagnostic(transparent)]
69 Toml(#[from] LocalProjectTomlValidationError),
70 #[diagnostic(transparent)]
71 RunCommand(#[from] RunCommandError),
72 #[diagnostic(transparent)]
73 LuaVersion(#[from] LuaVersionError),
74 #[diagnostic(transparent)]
75 RunLua(#[from] RunLuaError),
76 #[diagnostic(transparent)]
77 WorkspaceError(#[from] WorkspaceError),
78 #[diagnostic(transparent)]
79 WorkspaceTree(#[from] WorkspaceTreeError),
80 Io(#[from] std::io::Error),
81 #[diagnostic(transparent)]
82 Paths(#[from] PathsError),
83 #[error("No `run` field found in `lux.toml`")]
84 NoRunField,
85}
86
87#[derive(Builder)]
88#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
89pub struct Run<'a> {
90 workspace: &'a Workspace,
91 package: Option<PackageName>,
92 dir: Option<PathBuf>,
93 args: &'a [String],
94 config: &'a Config,
95 disable_loader: Option<bool>,
96}
97
98impl<State> RunBuilder<'_, State>
99where
100 State: run_builder::State + run_builder::IsComplete,
101{
102 pub async fn run(self) -> Result<(), RunError> {
103 let run = self._build();
104 let workspace = run.workspace;
105 let config = run.config;
106 let extra_args = run.args;
107 let project = workspace.single_member_or_select(&run.package)?;
108 let toml = project.toml().into_local()?;
109
110 let run_spec = toml
111 .run()
112 .ok_or(RunError::NoRunField)?
113 .current_platform()
114 .clone();
115
116 let mut args: Vec<String> = run_spec.args.map(|args| args.into()).unwrap_or_default();
117
118 if !extra_args.is_empty() {
119 args.extend(extra_args.iter().cloned());
120 }
121 let disable_loader = run.disable_loader.unwrap_or(false);
122 match &run_spec.command {
123 Some(command) => {
124 run_with_command(workspace, command, run.dir, disable_loader, &args, config).await
125 }
126 None => run_with_local_lua(workspace, run.dir, disable_loader, &args, config).await,
127 }
128 }
129}
130
131async fn run_with_local_lua(
132 workspace: &Workspace,
133 root_dir: Option<PathBuf>,
134 disable_loader: bool,
135 args: &Vec<String>,
136 config: &Config,
137) -> Result<(), RunError> {
138 let version = workspace.lua_version(config)?;
139
140 let tree = workspace.tree(config)?;
141
142 RunLua::new()
143 .root(&root_dir.unwrap_or(workspace.root().to_path_buf()))
144 .tree(&tree)
145 .config(config)
146 .lua_cmd(LuaBinary::new(version, config))
147 .disable_loader(disable_loader)
148 .args(args)
149 .run_lua()
150 .await?;
151
152 Ok(())
153}
154
155async fn run_with_command(
156 workspace: &Workspace,
157 command: &RunCommand,
158 root_dir: Option<PathBuf>,
159 disable_loader: bool,
160 args: &[String],
161 config: &Config,
162) -> Result<(), RunError> {
163 let tree = workspace.tree(config)?;
164 let paths = Paths::new(&tree)?;
165
166 let lua_init = if disable_loader {
167 None
168 } else if tree.version().lux_lib_dir().is_none() {
169 tracing::warn!("lux-lua library not found.\n Cannot use the `lux.loader`.\n To suppress this warning, set the `--no-loader` option.");
170 None
171 } else {
172 Some(paths.init())
173 };
174
175 let mut cmd = Command::new(command.deref());
176 if let Some(dir) = root_dir {
177 cmd.current_dir(dir);
178 } else {
179 cmd.current_dir(workspace.root());
180 }
181 match cmd
182 .args(args.iter().cloned().collect_vec())
183 .env("PATH", paths.path_prepended().joined())
184 .env("LUA_INIT", lua_init.unwrap_or_default())
185 .env("LUA_PATH", paths.package_path().joined())
186 .env("LUA_CPATH", paths.package_cpath().joined())
187 .status()
188 .await?
189 .code()
190 {
191 Some(0) => Ok(()),
192 code => Err(RunLuaError::LuaCommandNonZeroExitCode {
193 lua_cmd: command.to_string(),
194 exit_code: code,
195 }
196 .into()),
197 }
198}