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