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]
71 source: io::Error,
72 },
73 #[error("{cmd} exited with non-zero exit code: {}", exit_code.map(|code| code.to_string()).unwrap_or("unknown".into()))]
74 RunCommandNonZeroExitCode { cmd: String, exit_code: Option<i32> },
75 #[error(transparent)]
76 #[diagnostic(transparent)]
77 LuaVersionUnset(#[from] LuaVersionUnset),
78 #[error(transparent)]
79 #[diagnostic(transparent)]
80 Tree(#[from] TreeError),
81 #[error(transparent)]
82 #[diagnostic(transparent)]
83 Paths(#[from] PathsError),
84 #[error(transparent)]
85 #[diagnostic(transparent)]
86 LuaVersionError(#[from] LuaVersionError),
87 #[error(transparent)]
88 #[diagnostic(transparent)]
89 BuildProject(#[from] BuildWorkspaceError),
90 #[error(transparent)]
91 #[diagnostic(transparent)]
92 InstallCommand(#[from] InstallCommandError),
93 #[error(transparent)]
94 #[diagnostic(transparent)]
95 WorkspaceTree(#[from] WorkspaceTreeError),
96 #[error("failed to execute '{0}':\n{1}")]
97 Io(String, io::Error),
98}
99
100#[derive(Error, Debug, Diagnostic)]
101#[error(transparent)]
102pub enum InstallCommandError {
103 InstallError(#[from] InstallError),
104 PackageVersionReqError(#[from] PackageVersionReqError),
105 RemotePackageDBError(#[from] RemotePackageDBError),
106 Tree(#[from] TreeError),
107 LuaVersionUnset(#[from] LuaVersionUnset),
108}
109
110async fn exec(run: Exec<'_>) -> Result<(), ExecError> {
111 let lua_version = run
112 .workspace
113 .map(|project| project.lua_version(run.config))
114 .transpose()?
115 .unwrap_or(LuaVersion::from(run.config)?.clone());
116
117 if let Some(project) = run.workspace {
118 BuildWorkspace::new(project, run.config)
119 .no_lock(false)
120 .only_deps(false)
121 .build()
122 .await?;
123 } else if which(run.command).is_err() {
124 install_command(run.command, run.config).await?
125 };
126
127 let user_tree = run.config.user_tree(lua_version)?;
128 let mut paths = Paths::new(&user_tree)?;
129
130 if let Some(project) = run.workspace {
131 paths.prepend(&Paths::new(&project.tree(run.config)?)?);
132 }
133
134 let lua_init = if run.disable_loader.unwrap_or(false) {
135 None
136 } else if user_tree.version().lux_lib_dir().is_none() {
137 crate::logging::warn(
138 "⚠️ WARNING: lux-lua library not found.\n Cannot use the `lux.loader`.\n To suppress this warning, set the `--no-loader` option.".to_string(),
139 Some("exec".to_string()),
140 );
141 None
142 } else {
143 Some(paths.init())
144 };
145
146 let status = match Command::new(run.command)
147 .args(run.args)
148 .env("PATH", paths.path_prepended().joined())
149 .env("LUA_INIT", lua_init.unwrap_or_default())
150 .env("LUA_PATH", paths.package_path().joined())
151 .env("LUA_CPATH", paths.package_cpath().joined())
152 .status()
153 .await
154 {
155 Ok(status) => Ok(status),
156 Err(err) => Err(ExecError::RunCommandFailed {
157 cmd: run.command.to_string(),
158 source: err,
159 }),
160 }?;
161 if status.success() {
162 Ok(())
163 } else {
164 Err(ExecError::RunCommandNonZeroExitCode {
165 cmd: run.command.to_string(),
166 exit_code: status.code(),
167 })
168 }
169}
170
171async fn install_command(command: &str, config: &Config) -> Result<(), InstallCommandError> {
174 let install_spec = PackageInstallSpec::new(
175 PackageReq::new(command.into(), None)?,
176 tree::EntryType::Entrypoint,
177 )
178 .build();
179 let tree = config.user_tree(LuaVersion::from(config)?.clone())?;
180 Install::new(config)
181 .package(install_spec)
182 .tree(tree)
183 .install()
184 .await?;
185 Ok(())
186}