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