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