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