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)]
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 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}