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