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