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