Skip to main content

lux_lib/luarocks/
luarocks_installation.rs

1use miette::Diagnostic;
2use path_slash::{PathBufExt, PathExt};
3use ssri::Integrity;
4use std::{
5    io,
6    path::{Path, PathBuf},
7    process::ExitStatus,
8};
9use thiserror::Error;
10
11use crate::{
12    build::{self, BuildError},
13    config::Config,
14    fs,
15    lua_installation::LuaInstallation,
16    lua_version::{LuaVersion, LuaVersionUnset},
17    operations::UnpackError,
18    path::{Paths, PathsError},
19    tree::InstallTree,
20    variables::{self, VariableSubstitutionError},
21};
22
23#[cfg(target_family = "unix")]
24use crate::tree::{self, Tree, TreeError};
25#[cfg(target_family = "windows")]
26use crate::tree::{Tree, TreeError};
27
28#[cfg(target_family = "unix")]
29use crate::build::Build;
30
31#[cfg(target_family = "unix")]
32const LUAROCKS_EXE: &str = "luarocks";
33#[cfg(target_family = "windows")]
34const LUAROCKS_EXE: &str = "luarocks.exe";
35
36pub(crate) const LUAROCKS_VERSION: &str = "3.13.0-1";
37
38#[cfg(target_family = "unix")]
39const LUAROCKS_ROCKSPEC: &str = "
40rockspec_format = '3.0'
41package = 'luarocks'
42version = '3.13.0-1'
43source = {
44    url = 'git+https://github.com/luarocks/luarocks',
45    tag = 'v3.13.0',
46}
47build = {
48    type = 'builtin',
49}
50";
51
52#[derive(Error, Debug, Diagnostic)]
53#[non_exhaustive]
54pub enum LuaRocksError {
55    #[error(transparent)]
56    #[diagnostic(transparent)]
57    LuaVersionUnset(#[from] LuaVersionUnset),
58    // #[error(transparent)]
59    // Io(#[from] io::Error),
60    #[error(transparent)]
61    #[diagnostic(transparent)]
62    Tree(#[from] TreeError),
63}
64
65#[derive(Error, Debug, Diagnostic)]
66#[non_exhaustive]
67pub enum LuaRocksInstallError {
68    #[error("IO error occurred while trying to install luarocks")]
69    Io(#[from] io::Error),
70    #[error(transparent)]
71    #[diagnostic(transparent)]
72    Fs(#[from] fs::FsError),
73    #[error(transparent)]
74    #[diagnostic(transparent)]
75    Tree(#[from] TreeError),
76    #[error(transparent)]
77    #[diagnostic(transparent)]
78    BuildError(#[from] Box<BuildError>),
79    #[error(transparent)]
80    Request(#[from] reqwest::Error),
81    #[error(transparent)]
82    #[diagnostic(transparent)]
83    UnpackError(#[from] UnpackError),
84    #[error("luarocks integrity mismatch.\nExpected: {expected}\nBut got: {got}")]
85    IntegrityMismatch { expected: Integrity, got: Integrity },
86}
87
88impl From<BuildError> for LuaRocksInstallError {
89    fn from(source: BuildError) -> Self {
90        Self::BuildError(Box::new(source))
91    }
92}
93
94#[derive(Error, Debug, Diagnostic)]
95#[non_exhaustive]
96pub enum ExecLuaRocksError {
97    #[error(transparent)]
98    #[diagnostic(transparent)]
99    LuaVersionUnset(#[from] LuaVersionUnset),
100    #[error("could not substitute '$(LUA_LIBDIR)' and '$(LUA_INCDIR)' variables in the luarocks config template")]
101    #[diagnostic(forward(0))]
102    VariableSubstitutionInConfig(#[from] VariableSubstitutionError),
103    #[error("failed to run luarocks")]
104    Io(#[from] io::Error),
105    #[error(transparent)]
106    #[diagnostic(transparent)]
107    Fs(#[from] fs::FsError),
108    #[error("error setting up luarocks paths")]
109    #[diagnostic(forward(0))]
110    Paths(#[from] PathsError),
111    #[error("luarocks binary not found at '{0}'")]
112    #[diagnostic(help(
113        "Lux successfully installed luarocks, but couldn't find the binary to execute"
114    ))]
115    LuarocksBinNotFound(PathBuf),
116    #[error(
117        r#"executing luarocks compatibility layer failed.
118status: {status}
119stdout:
120{stdout}
121stderr:
122{stderr}
123"#
124    )]
125    #[diagnostic(help("see the build output for details"))]
126    CommandFailure {
127        status: ExitStatus,
128        stdout: String,
129        stderr: String,
130    },
131}
132
133pub struct LuaRocksInstallation {
134    tree: Tree,
135    config: Config,
136}
137
138impl LuaRocksInstallation {
139    pub fn new(config: &Config, tree: Tree) -> Result<Self, LuaRocksError> {
140        let luarocks_installation = Self {
141            tree,
142            config: config.clone(),
143        };
144        Ok(luarocks_installation)
145    }
146
147    #[cfg(target_family = "unix")]
148    #[tracing::instrument(level = "trace", skip(self))]
149    pub async fn ensure_installed(
150        &self,
151        lua: &LuaInstallation,
152    ) -> Result<(), LuaRocksInstallError> {
153        use crate::{lua_rockspec::RemoteLuaRockspec, package::PackageReq};
154
155        let mut lockfile = self.tree.lockfile()?.write_guard();
156
157        let luarocks_req =
158            unsafe { PackageReq::new_unchecked("luarocks".into(), Some(LUAROCKS_VERSION.into())) };
159
160        if !self.tree.match_rocks(&luarocks_req)?.is_found() {
161            let rockspec = unsafe { RemoteLuaRockspec::new(LUAROCKS_ROCKSPEC).unwrap_unchecked() };
162            let pkg = Build::new()
163                .rockspec(&rockspec)
164                .lua(lua)
165                .tree(&self.tree)
166                .entry_type(tree::EntryType::Entrypoint)
167                .config(&self.config)
168                .constraint(luarocks_req.version_req().clone().into())
169                .build()
170                .await?;
171            lockfile.add_entrypoint(&pkg);
172        }
173        Ok(())
174    }
175
176    #[cfg(target_family = "windows")]
177    #[tracing::instrument(level = "trace", skip(self))]
178    pub async fn ensure_installed(
179        &self,
180        _lua: &LuaInstallation,
181    ) -> Result<(), LuaRocksInstallError> {
182        use crate::{hash::HasIntegrity, operations};
183        use std::io::Cursor;
184        let file_name = "luarocks-3.13.0-windows-64";
185        let url = format!("https://luarocks.github.io/luarocks/releases/{file_name}.zip");
186        let response = reqwest::get(url).await?.error_for_status()?.bytes().await?;
187        let hash = response.hash().await?;
188        let expected_hash: Integrity = unsafe {
189            "sha256-CJet5dRZ1VzRliqUgVN0WmdJ/rNFQDxoqqkgc4hVerk="
190                .parse()
191                .unwrap_unchecked()
192        };
193        if expected_hash.matches(&hash).is_none() {
194            return Err(LuaRocksInstallError::IntegrityMismatch {
195                expected: expected_hash,
196                got: hash,
197            });
198        }
199        let cursor = Cursor::new(response);
200        let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
201        let unpack_dir = fs::tempfile::tempdir()?;
202        operations::unpack(
203            mime_type,
204            cursor,
205            false,
206            format!("{file_name}.zip"),
207            unpack_dir.path(),
208        )
209        .await?;
210        let luarocks_exe = unpack_dir.path().join(file_name).join(LUAROCKS_EXE);
211        fs::tokio::copy(luarocks_exe, &self.tree.bin().join(LUAROCKS_EXE)).await?;
212
213        Ok(())
214    }
215
216    #[tracing::instrument(level = "trace", skip(self))]
217    pub async fn make(
218        self,
219        rockspec_path: &Path,
220        build_dir: &Path,
221        dest_dir: &Path,
222        lua: &LuaInstallation,
223    ) -> Result<(), ExecLuaRocksError> {
224        fs::sync::create_dir_all(dest_dir)?;
225        let dest_dir_str = dest_dir.to_slash_lossy().to_string();
226        let rockspec_path_str = rockspec_path.to_slash_lossy().to_string();
227        let args = vec![
228            "make",
229            "--deps-mode",
230            "none",
231            "--tree",
232            &dest_dir_str,
233            &rockspec_path_str,
234        ];
235        self.exec(args, build_dir, lua).await
236    }
237
238    async fn exec(
239        self,
240        args: Vec<&str>,
241        cwd: &Path,
242        lua: &LuaInstallation,
243    ) -> Result<(), ExecLuaRocksError> {
244        let luarocks_paths = Paths::new(&self.tree)?;
245        // Ensure a pure environment so we can do parallel builds
246        let temp_dir = fs::tempfile::tempdir()?;
247        let lua_version_str = match lua.version {
248            LuaVersion::Lua51 | LuaVersion::LuaJIT => "5.1",
249            LuaVersion::Lua52 | LuaVersion::LuaJIT52 => "5.2",
250            LuaVersion::Lua53 => "5.3",
251            LuaVersion::Lua54 => "5.4",
252            LuaVersion::Lua55 => "5.5",
253        };
254        let luarocks_config_content = format!(
255            r#"
256lua_version = "{0}"
257variables = {{
258    LUA_LIBDIR = "$(LUA_LIBDIR)",
259    LUA_INCDIR = "$(LUA_INCDIR)",
260    LUA_VERSION = "{1}",
261    MAKE = "{2}",
262}}
263"#,
264            lua_version_str,
265            LuaVersion::from(&self.config)?,
266            self.config.make_cmd()
267        );
268        let luarocks_config_content =
269            variables::substitute(&[lua, &self.config], &luarocks_config_content)?;
270        let luarocks_config = temp_dir.path().join("luarocks-config.lua");
271        fs::sync::write(luarocks_config.clone(), luarocks_config_content)?;
272        let luarocks_bin = self.tree.bin().join(LUAROCKS_EXE);
273        if !luarocks_bin.is_file() {
274            return Err(ExecLuaRocksError::LuarocksBinNotFound(luarocks_bin));
275        }
276        let output = self
277            .config
278            .wrapped_command(luarocks_bin, args)
279            .current_dir(cwd)
280            .env("PATH", luarocks_paths.path_prepended().joined())
281            .env("LUA_PATH", luarocks_paths.package_path().joined())
282            .env("LUA_CPATH", luarocks_paths.package_cpath().joined())
283            .env("HOME", temp_dir.path().to_slash_lossy().to_string())
284            .env(
285                "LUAROCKS_CONFIG",
286                luarocks_config.to_slash_lossy().to_string(),
287            )
288            .output()
289            .await?;
290        if output.status.success() {
291            build::utils::trace_command_output(&output);
292            Ok(())
293        } else {
294            Err(ExecLuaRocksError::CommandFailure {
295                status: output.status,
296                stdout: String::from_utf8_lossy(&output.stdout).into(),
297                stderr: String::from_utf8_lossy(&output.stderr).into(),
298            })
299        }
300    }
301}