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