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