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}
134
135pub struct LuaRocksInstallation {
136    tree: Tree,
137    config: Config,
138}
139
140impl LuaRocksInstallation {
141    pub fn new(config: &Config, tree: Tree) -> Result<Self, LuaRocksError> {
142        let luarocks_installation = Self {
143            tree,
144            config: config.clone(),
145        };
146        Ok(luarocks_installation)
147    }
148
149    #[cfg(target_family = "unix")]
150    #[tracing::instrument(level = "trace", skip(self))]
151    pub async fn ensure_installed(
152        &self,
153        lua: &LuaInstallation,
154    ) -> Result<(), LuaRocksInstallError> {
155        use crate::{lua_rockspec::RemoteLuaRockspec, package::PackageReq};
156
157        let mut lockfile = self.tree.lockfile()?.write_guard();
158
159        let luarocks_req =
160            unsafe { PackageReq::new_unchecked("luarocks".into(), Some(LUAROCKS_VERSION.into())) };
161
162        if !self.tree.match_rocks(&luarocks_req)?.is_found() {
163            let rockspec = unsafe { RemoteLuaRockspec::new(LUAROCKS_ROCKSPEC).unwrap_unchecked() };
164            let pkg = Build::new()
165                .rockspec(&rockspec)
166                .lua(lua)
167                .tree(&self.tree)
168                .entry_type(tree::EntryType::Entrypoint)
169                .config(&self.config)
170                .behaviour(build::BuildBehaviour::Ignore)
171                .constraint(luarocks_req.version_req().clone().into())
172                .build()
173                .await?;
174            lockfile.add_entrypoint(&pkg);
175        }
176        Ok(())
177    }
178
179    #[cfg(target_family = "windows")]
180    #[tracing::instrument(level = "trace", skip(self))]
181    pub async fn ensure_installed(
182        &self,
183        _lua: &LuaInstallation,
184    ) -> Result<(), LuaRocksInstallError> {
185        use crate::{hash::HasIntegrity, operations};
186        use std::io::Cursor;
187        let file_name = "luarocks-3.13.0-windows-64";
188        let url = format!("https://luarocks.github.io/luarocks/releases/{file_name}.zip");
189        let response = reqwest::get(url).await?.error_for_status()?.bytes().await?;
190        let hash = response.hash().await?;
191        let expected_hash: Integrity = unsafe {
192            "sha256-CJet5dRZ1VzRliqUgVN0WmdJ/rNFQDxoqqkgc4hVerk="
193                .parse()
194                .unwrap_unchecked()
195        };
196        if expected_hash.matches(&hash).is_none() {
197            return Err(LuaRocksInstallError::IntegrityMismatch {
198                expected: expected_hash,
199                got: hash,
200            });
201        }
202        let cursor = Cursor::new(response);
203        let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
204        let unpack_dir = fs::tempfile::tempdir()?;
205        operations::unpack(
206            mime_type,
207            cursor,
208            false,
209            format!("{file_name}.zip"),
210            unpack_dir.path(),
211        )
212        .await?;
213        let luarocks_exe = unpack_dir.path().join(file_name).join(LUAROCKS_EXE);
214        fs::tokio::copy(luarocks_exe, &self.tree.bin().join(LUAROCKS_EXE)).await?;
215
216        Ok(())
217    }
218
219    #[tracing::instrument(level = "trace", skip(self))]
220    pub async fn make(
221        self,
222        rockspec_path: &Path,
223        build_dir: &Path,
224        dest_dir: &Path,
225        lua: &LuaInstallation,
226    ) -> Result<(), ExecLuaRocksError> {
227        fs::sync::create_dir_all(dest_dir)?;
228        let dest_dir_str = dest_dir.to_slash_lossy().to_string();
229        let rockspec_path_str = rockspec_path.to_slash_lossy().to_string();
230        let args = vec![
231            "make",
232            "--deps-mode",
233            "none",
234            "--tree",
235            &dest_dir_str,
236            &rockspec_path_str,
237        ];
238        self.exec(args, build_dir, lua).await
239    }
240
241    async fn exec(
242        self,
243        args: Vec<&str>,
244        cwd: &Path,
245        lua: &LuaInstallation,
246    ) -> Result<(), ExecLuaRocksError> {
247        let luarocks_paths = Paths::new(&self.tree)?;
248        // Ensure a pure environment so we can do parallel builds
249        let temp_dir = fs::tempfile::tempdir()?;
250        let lua_version_str = match lua.version {
251            LuaVersion::Lua51 | LuaVersion::LuaJIT => "5.1",
252            LuaVersion::Lua52 | LuaVersion::LuaJIT52 => "5.2",
253            LuaVersion::Lua53 => "5.3",
254            LuaVersion::Lua54 => "5.4",
255            LuaVersion::Lua55 => "5.5",
256        };
257        let luarocks_config_content = format!(
258            r#"
259lua_version = "{0}"
260variables = {{
261    LUA_LIBDIR = "$(LUA_LIBDIR)",
262    LUA_INCDIR = "$(LUA_INCDIR)",
263    LUA_VERSION = "{1}",
264    MAKE = "{2}",
265}}
266"#,
267            lua_version_str,
268            LuaVersion::from(&self.config)?,
269            self.config.make_cmd()
270        );
271        let luarocks_config_content =
272            variables::substitute(&[lua, &self.config], &luarocks_config_content)?;
273        let luarocks_config = temp_dir.path().join("luarocks-config.lua");
274        fs::sync::write(luarocks_config.clone(), luarocks_config_content)?;
275        let luarocks_bin = self.tree.bin().join(LUAROCKS_EXE);
276        if !luarocks_bin.is_file() {
277            return Err(ExecLuaRocksError::LuarocksBinNotFound(luarocks_bin));
278        }
279        let output = self
280            .config
281            .wrapped_command(luarocks_bin, args)
282            .current_dir(cwd)
283            .env("PATH", luarocks_paths.path_prepended().joined())
284            .env("LUA_PATH", luarocks_paths.package_path().joined())
285            .env("LUA_CPATH", luarocks_paths.package_cpath().joined())
286            .env("HOME", temp_dir.path().to_slash_lossy().to_string())
287            .env(
288                "LUAROCKS_CONFIG",
289                luarocks_config.to_slash_lossy().to_string(),
290            )
291            .output()
292            .await?;
293        if output.status.success() {
294            build::utils::trace_command_output(&output);
295            Ok(())
296        } else {
297            Err(ExecLuaRocksError::CommandFailure {
298                status: output.status,
299                stdout: String::from_utf8_lossy(&output.stdout).into(),
300                stderr: String::from_utf8_lossy(&output.stderr).into(),
301            })
302        }
303    }
304}