Skip to main content

lux_lib/operations/
exec.rs

1use 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/// Rocks package runner, providing fine-grained control
22/// over how a package should be run.
23#[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
164/// Ensure that a command is installed.
165/// This defaults to the local project tree if cwd is a project root.
166async 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}