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