Skip to main content

lux_lib/operations/
exec.rs

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