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::{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/// Rocks package runner, providing fine-grained control
24/// over how a package should be run.
25#[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        eprintln!(
138            "⚠️ WARNING: lux-lua library not found.
139    Cannot use the `lux.loader`.
140    To suppress this warning, set the `--no-loader` option.
141                    "
142        );
143        None
144    } else {
145        Some(paths.init())
146    };
147
148    let status = match Command::new(run.command)
149        .args(run.args)
150        .env("PATH", paths.path_prepended().joined())
151        .env("LUA_INIT", lua_init.unwrap_or_default())
152        .env("LUA_PATH", paths.package_path().joined())
153        .env("LUA_CPATH", paths.package_cpath().joined())
154        .status()
155        .await
156    {
157        Ok(status) => Ok(status),
158        Err(err) => Err(ExecError::RunCommandFailed {
159            cmd: run.command.to_string(),
160            source: err,
161        }),
162    }?;
163    if status.success() {
164        Ok(())
165    } else {
166        Err(ExecError::RunCommandNonZeroExitCode {
167            cmd: run.command.to_string(),
168            exit_code: status.code(),
169        })
170    }
171}
172
173/// Ensure that a command is installed.
174/// This defaults to the local project tree if cwd is a project root.
175async fn install_command(command: &str, config: &Config) -> Result<(), InstallCommandError> {
176    let install_spec = PackageInstallSpec::new(
177        PackageReq::new(command.into(), None)?,
178        tree::EntryType::Entrypoint,
179    )
180    .build();
181    let tree = config.user_tree(LuaVersion::from(config)?.clone())?;
182    Install::new(config)
183        .package(install_spec)
184        .tree(tree)
185        .install()
186        .await?;
187    Ok(())
188}