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