lux_lib/operations/
exec.rs1use std::io;
2use tokio::process::Command;
3
4use crate::{
5 build::BuildBehaviour,
6 config::Config,
7 drivers::install_packages::{InstallPackages, InstallPackagesError},
8 lua_rockspec::LuaVersionError,
9 lua_version::{LuaVersion, LuaVersionUnset},
10 operations::{BuildWorkspace, BuildWorkspaceError},
11 package::{PackageReq, PackageVersionReqError},
12 package_db::{PackageDB, PackageDBError},
13 path::{Paths, PathsError},
14 tree::{self, InstallTree, TreeError},
15 workspace::{Workspace, WorkspaceTreeError},
16};
17use bon::Builder;
18use itertools::Itertools;
19use miette::Diagnostic;
20use thiserror::Error;
21use which::which;
22
23use super::PackageInstallSpec;
24
25#[derive(Builder)]
28#[builder(start_fn = new, finish_fn(name = _exec, vis = ""))]
29pub struct Exec<'a> {
30 #[builder(start_fn)]
31 command: &'a str,
32 #[builder(start_fn)]
33 workspace: Option<&'a Workspace>,
34 #[builder(start_fn)]
35 config: &'a Config,
36
37 #[builder(field)]
38 args: Vec<String>,
39
40 disable_loader: Option<bool>,
41}
42
43impl<State: exec_builder::State> ExecBuilder<'_, State> {
44 pub fn arg(mut self, arg: impl Into<String>) -> Self {
45 self.args.push(arg.into());
46 self
47 }
48
49 pub fn args(mut self, args: impl IntoIterator<Item: Into<String>>) -> Self {
50 self.args.extend(args.into_iter().map_into());
51 self
52 }
53}
54
55impl<State> ExecBuilder<'_, State>
56where
57 State: exec_builder::State + exec_builder::IsComplete,
58{
59 pub async fn exec(self) -> Result<(), ExecError>
60 where
61 State: exec_builder::IsComplete,
62 {
63 exec(self._exec()).await
64 }
65}
66
67#[derive(Error, Debug, Diagnostic)]
68pub enum ExecError {
69 #[error("failed to run {cmd}: {source}")]
70 RunCommandFailed {
71 cmd: String,
72 source: io::Error,
73 #[help]
74 help: Option<String>,
75 },
76 #[error(
77 "{cmd} exited with non-zero exit code: {}",
78 exit_code.map(|code| code.to_string()).unwrap_or("unknown".into())
79 )]
80 #[diagnostic(help("check the command's output for errors."))]
81 RunCommandNonZeroExitCode { cmd: String, exit_code: Option<i32> },
82 #[error(transparent)]
83 #[diagnostic(transparent)]
84 LuaVersionUnset(#[from] LuaVersionUnset),
85 #[error(transparent)]
86 #[diagnostic(transparent)]
87 Tree(#[from] TreeError),
88 #[error(transparent)]
89 #[diagnostic(transparent)]
90 Paths(#[from] PathsError),
91 #[error(transparent)]
92 #[diagnostic(transparent)]
93 LuaVersionError(#[from] LuaVersionError),
94 #[error(transparent)]
95 #[diagnostic(transparent)]
96 BuildWorkspace(#[from] Box<BuildWorkspaceError>),
97 #[error(transparent)]
98 #[diagnostic(transparent)]
99 InstallCommand(#[from] Box<InstallCommandError>),
100 #[error(transparent)]
101 #[diagnostic(transparent)]
102 WorkspaceTree(#[from] WorkspaceTreeError),
103 #[error("failed to execute '{0}':\n{1}")]
104 Io(String, io::Error),
105}
106
107impl From<BuildWorkspaceError> for ExecError {
108 fn from(source: BuildWorkspaceError) -> Self {
109 Self::BuildWorkspace(Box::new(source))
110 }
111}
112
113impl From<InstallCommandError> for ExecError {
114 fn from(source: InstallCommandError) -> Self {
115 Self::InstallCommand(Box::new(source))
116 }
117}
118
119#[derive(Error, Debug, Diagnostic)]
120#[error(transparent)]
121pub enum InstallCommandError {
122 InstallError(#[from] Box<InstallPackagesError>),
123 PackageVersionReqError(#[from] PackageVersionReqError),
124 PackageDBError(#[from] PackageDBError),
125 Tree(#[from] TreeError),
126 LuaVersionUnset(#[from] LuaVersionUnset),
127}
128
129impl From<InstallPackagesError> for InstallCommandError {
130 fn from(source: InstallPackagesError) -> Self {
131 Self::InstallError(Box::new(source))
132 }
133}
134
135async fn exec(run: Exec<'_>) -> Result<(), ExecError> {
136 let lua_version = run
137 .workspace
138 .map(|project| project.lua_version(run.config))
139 .transpose()?
140 .unwrap_or(LuaVersion::from(run.config)?.clone());
141
142 if let Some(project) = run.workspace {
143 BuildWorkspace::new(project, run.config)
144 .no_lock(false)
145 .only_deps(false)
146 .build()
147 .await?;
148 } else if which(run.command).is_err() {
149 install_command(run.command, run.config).await?
150 };
151
152 let user_tree = run.config.user_tree(lua_version.clone())?;
153
154 let paths = if let Some(project) = run.workspace {
155 let workspace_paths = Paths::new(&project.tree(run.config)?)?;
156 match Paths::new(&user_tree) {
157 Ok(mut user_paths) => {
158 user_paths.prepend(&workspace_paths);
159 user_paths
160 }
161 Err(err) => {
162 tracing::warn!(
163 "failed to load the user tree at '{}': {err}",
164 user_tree.root().display()
165 );
166 workspace_paths
167 }
168 }
169 } else {
170 Paths::new(&user_tree)?
171 };
172
173 let lua_init = if user_tree.version().is_luau() || run.disable_loader.unwrap_or(false) {
174 None
175 } else if lua_version.lux_lib_dir().is_none() {
176 tracing::warn!(
177 r#"lux-lua library not found.
178Cannot use the `lux.loader`.
179To suppress this warning, set the `--no-loader` option."#
180 );
181 None
182 } else {
183 Some(paths.init())
184 };
185
186 let status = match Command::new(run.command)
187 .args(run.args)
188 .env("PATH", paths.path_prepended().joined())
189 .env("LUA_INIT", lua_init.unwrap_or_default())
190 .env("LUA_PATH", paths.package_path().joined())
191 .env("LUA_CPATH", paths.package_cpath().joined())
192 .status()
193 .await
194 {
195 Ok(status) => Ok(status),
196 Err(err) => {
197 let help = if err.to_string().starts_with("No such file") {
198 Some(format!(
199 "make sure '{}' is available on your PATH",
200 run.command
201 ))
202 } else {
203 None
204 };
205 Err(ExecError::RunCommandFailed {
206 cmd: run.command.to_string(),
207 source: err,
208 help,
209 })
210 }
211 }?;
212 if status.success() {
213 Ok(())
214 } else {
215 Err(ExecError::RunCommandNonZeroExitCode {
216 cmd: run.command.to_string(),
217 exit_code: status.code(),
218 })
219 }
220}
221
222async fn install_command(command: &str, config: &Config) -> Result<(), InstallCommandError> {
225 let package = PackageReq::new(command.into(), None)?;
226 let tree = config.user_tree(LuaVersion::from(config)?.clone())?;
227
228 if tree
229 .match_rocks(&package)
230 .is_ok_and(|matches| matches.is_found())
231 {
232 return Ok(());
233 }
234 let install_spec = PackageInstallSpec::new(package, tree::EntryType::Entrypoint)
235 .build_behaviour(BuildBehaviour::Force)
236 .build();
237 InstallPackages::new(config, &tree)
238 .package(install_spec)
239 .package_db(PackageDB::from_config(config).await?)
240 .install()
241 .await?;
242 Ok(())
243}