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