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