lux_lib/operations/
exec.rs1use std::io;
2use tokio::process::Command;
3
4use crate::{
5 config::{Config, LuaVersion, LuaVersionUnset},
6 lua_rockspec::LuaVersionError,
7 operations::{BuildProject, BuildProjectError, Install},
8 package::{PackageReq, PackageVersionReqError},
9 path::{Paths, PathsError},
10 project::{Project, ProjectTreeError},
11 remote_package_db::RemotePackageDBError,
12 tree::{self, TreeError},
13};
14use bon::Builder;
15use itertools::Itertools;
16use thiserror::Error;
17use which::which;
18
19use super::{InstallError, PackageInstallSpec};
20
21#[derive(Builder)]
24#[builder(start_fn = new, finish_fn(name = _exec, vis = ""))]
25pub struct Exec<'a> {
26 #[builder(start_fn)]
27 command: &'a str,
28 #[builder(start_fn)]
29 project: Option<&'a Project>,
30 #[builder(start_fn)]
31 config: &'a Config,
32
33 #[builder(field)]
34 args: Vec<String>,
35
36 disable_loader: Option<bool>,
37}
38
39impl<State: exec_builder::State> ExecBuilder<'_, State> {
40 pub fn arg(mut self, arg: impl Into<String>) -> Self {
41 self.args.push(arg.into());
42 self
43 }
44
45 pub fn args(mut self, args: impl IntoIterator<Item: Into<String>>) -> Self {
46 self.args.extend(args.into_iter().map_into());
47 self
48 }
49}
50
51impl<State> ExecBuilder<'_, State>
52where
53 State: exec_builder::State + exec_builder::IsComplete,
54{
55 pub async fn exec(self) -> Result<(), ExecError>
56 where
57 State: exec_builder::IsComplete,
58 {
59 exec(self._exec()).await
60 }
61}
62
63#[derive(Error, Debug)]
64pub enum ExecError {
65 #[error("failed to run {cmd}: {source}")]
66 RunCommandFailed {
67 cmd: String,
68 #[source]
69 source: io::Error,
70 },
71 #[error("{cmd} exited with non-zero exit code: {}", exit_code.map(|code| code.to_string()).unwrap_or("unknown".into()))]
72 RunCommandNonZeroExitCode { cmd: String, exit_code: Option<i32> },
73 #[error(transparent)]
74 LuaVersionUnset(#[from] LuaVersionUnset),
75 #[error(transparent)]
76 Tree(#[from] TreeError),
77 #[error(transparent)]
78 Paths(#[from] PathsError),
79 #[error(transparent)]
80 LuaVersionError(#[from] LuaVersionError),
81 #[error(transparent)]
82 BuildProject(#[from] BuildProjectError),
83 #[error(transparent)]
84 InstallCommand(#[from] InstallCommandError),
85 #[error(transparent)]
86 ProjectTreeError(#[from] ProjectTreeError),
87 #[error("failed to execute `{0}`:\n{1}")]
88 Io(String, io::Error),
89}
90
91#[derive(Error, Debug)]
92#[error(transparent)]
93pub enum InstallCommandError {
94 InstallError(#[from] InstallError),
95 PackageVersionReqError(#[from] PackageVersionReqError),
96 RemotePackageDBError(#[from] RemotePackageDBError),
97 Tree(#[from] TreeError),
98 LuaVersionUnset(#[from] LuaVersionUnset),
99}
100
101async fn exec(run: Exec<'_>) -> Result<(), ExecError> {
102 let lua_version = run
103 .project
104 .map(|project| project.lua_version(run.config))
105 .transpose()?
106 .unwrap_or(LuaVersion::from(run.config)?.clone());
107
108 if let Some(project) = run.project {
109 BuildProject::new(project, run.config)
110 .no_lock(false)
111 .only_deps(false)
112 .build()
113 .await?;
114 } else if which(run.command).is_err() {
115 install_command(run.command, run.config).await?
116 };
117
118 let user_tree = run.config.user_tree(lua_version)?;
119 let mut paths = Paths::new(&user_tree)?;
120
121 if let Some(project) = run.project {
122 paths.prepend(&Paths::new(&project.tree(run.config)?)?);
123 }
124
125 let lua_init = if run.disable_loader.unwrap_or(false) {
126 None
127 } else if user_tree.version().lux_lib_dir().is_none() {
128 eprintln!(
129 "⚠️ WARNING: lux-lua library not found.
130 Cannot use the `lux.loader`.
131 To suppress this warning, set the `--no-loader` option.
132 "
133 );
134 None
135 } else {
136 Some(paths.init())
137 };
138
139 let status = match Command::new(run.command)
140 .args(run.args)
141 .env("PATH", paths.path_prepended().joined())
142 .env("LUA_INIT", lua_init.unwrap_or_default())
143 .env("LUA_PATH", paths.package_path().joined())
144 .env("LUA_CPATH", paths.package_cpath().joined())
145 .status()
146 .await
147 {
148 Ok(status) => Ok(status),
149 Err(err) => Err(ExecError::RunCommandFailed {
150 cmd: run.command.to_string(),
151 source: err,
152 }),
153 }?;
154 if status.success() {
155 Ok(())
156 } else {
157 Err(ExecError::RunCommandNonZeroExitCode {
158 cmd: run.command.to_string(),
159 exit_code: status.code(),
160 })
161 }
162}
163
164async fn install_command(command: &str, config: &Config) -> Result<(), InstallCommandError> {
167 let install_spec = PackageInstallSpec::new(
168 PackageReq::new(command.into(), None)?,
169 tree::EntryType::Entrypoint,
170 )
171 .build();
172 let tree = config.user_tree(LuaVersion::from(config)?.clone())?;
173 Install::new(config)
174 .package(install_spec)
175 .tree(tree)
176 .install()
177 .await?;
178 Ok(())
179}