Skip to main content

lux_lib/luarocks/
luarocks_installation.rs

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