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