1use futures::future::join_all;
2use itertools::Itertools;
3use path_slash::{PathBufExt, PathExt};
4use ssri::Integrity;
5use std::{
6 collections::HashMap,
7 io,
8 path::{Path, PathBuf},
9 process::ExitStatus,
10 sync::Arc,
11};
12use tempdir::TempDir;
13use thiserror::Error;
14use tokio::process::Command;
15
16use crate::{
17 build::{Build, BuildError},
18 config::{Config, LuaVersion, LuaVersionUnset},
19 lockfile::{LocalPackage, LocalPackageId},
20 lua_installation::LuaInstallation,
21 lua_rockspec::RockspecFormat,
22 operations::{get_all_dependencies, PackageInstallSpec, SearchAndDownloadError, UnpackError},
23 path::{Paths, PathsError},
24 progress::{MultiProgress, Progress, ProgressBar},
25 remote_package_db::{RemotePackageDB, RemotePackageDBError},
26 rockspec::Rockspec,
27 tree::{self, Tree, TreeError},
28 variables::{self, VariableSubstitutionError},
29};
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.11.1-1";
37
38#[cfg(target_family = "unix")]
39const LUAROCKS_ROCKSPEC: &str = "
40rockspec_format = '3.0'
41package = 'luarocks'
42version = '3.11.1-1'
43source = {
44 url = 'git+https://github.com/luarocks/luarocks',
45 tag = 'v3.11.1',
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 InstallBuildDependenciesError {
80 #[error(transparent)]
81 Io(#[from] io::Error),
82 #[error(transparent)]
83 Tree(#[from] TreeError),
84 #[error(transparent)]
85 RemotePackageDBError(#[from] RemotePackageDBError),
86 #[error(transparent)]
87 SearchAndDownloadError(#[from] SearchAndDownloadError),
88 #[error(transparent)]
89 BuildError(#[from] BuildError),
90}
91
92#[derive(Error, Debug)]
93pub enum ExecLuaRocksError {
94 #[error(transparent)]
95 LuaVersionUnset(#[from] LuaVersionUnset),
96 #[error("could not write luarocks config: {0}")]
97 WriteLuarocksConfigError(io::Error),
98 #[error("could not write luarocks config: {0}")]
99 VariableSubstitutionInConfig(#[from] VariableSubstitutionError),
100 #[error("failed to run luarocks: {0}")]
101 Io(#[from] io::Error),
102 #[error("error setting up luarocks paths: {0}")]
103 Paths(#[from] PathsError),
104 #[error("luarocks binary not found at {0}")]
105 LuarocksBinNotFound(PathBuf),
106 #[error("executing luarocks compatibility layer failed.\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}")]
107 CommandFailure {
108 status: ExitStatus,
109 stdout: String,
110 stderr: String,
111 },
112}
113
114pub struct LuaRocksInstallation {
115 tree: Tree,
116 config: Config,
117}
118
119impl LuaRocksInstallation {
120 pub fn new(config: &Config, tree: Tree) -> Result<Self, LuaRocksError> {
121 let luarocks_installation = Self {
122 tree,
123 config: config.clone(),
124 };
125 Ok(luarocks_installation)
126 }
127
128 #[cfg(target_family = "unix")]
129 pub async fn ensure_installed(
130 &self,
131 progress: &Progress<ProgressBar>,
132 ) -> Result<(), LuaRocksInstallError> {
133 use crate::{lua_rockspec::RemoteLuaRockspec, package::PackageReq};
134
135 let mut lockfile = self.tree.lockfile()?.write_guard();
136
137 let luarocks_req =
138 PackageReq::new("luarocks".into(), Some(LUAROCKS_VERSION.into())).unwrap();
139
140 if !self.tree.match_rocks(&luarocks_req)?.is_found() {
141 let rockspec = RemoteLuaRockspec::new(LUAROCKS_ROCKSPEC).unwrap();
142 let pkg = Build::new(
143 &rockspec,
144 &self.tree,
145 tree::EntryType::Entrypoint,
146 &self.config,
147 progress,
148 )
149 .constraint(luarocks_req.version_req().clone().into())
150 .build()
151 .await?;
152 lockfile.add_entrypoint(&pkg);
153 }
154 Ok(())
155 }
156
157 #[cfg(target_family = "windows")]
158 pub async fn ensure_installed(
159 &self,
160 progress: &Progress<ProgressBar>,
161 ) -> Result<(), LuaRocksInstallError> {
162 use crate::{hash::HasIntegrity, operations};
163 use std::io::Cursor;
164 let url = "https://luarocks.github.io/luarocks/releases/luarocks-3.11.1-windows-64.zip";
165 let response = reqwest::get(url.to_owned())
166 .await?
167 .error_for_status()?
168 .bytes()
169 .await?;
170 let hash = response.hash()?;
171 let expected_hash: Integrity = "sha256-xx26PQPhIwXpzNAixiHIhpq6PRJNkkniFK7VwW82gqM="
172 .parse()
173 .unwrap();
174 if expected_hash.matches(&hash).is_none() {
175 return Err(LuaRocksInstallError::IntegrityMismatch {
176 expected: expected_hash,
177 got: hash,
178 });
179 }
180 let cursor = Cursor::new(response);
181 let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
182 let unpack_dir = TempDir::new("luarocks-exe")?.into_path();
183 operations::unpack(
184 mime_type,
185 cursor,
186 false,
187 "luarocks-3.11.1-windows-64.zip".into(),
188 &unpack_dir,
189 progress,
190 )
191 .await?;
192 let luarocks_exe = unpack_dir
193 .join("luarocks-3.11.1-windows-64")
194 .join(LUAROCKS_EXE);
195 tokio::fs::copy(luarocks_exe, &self.tree.bin().join(LUAROCKS_EXE)).await?;
196
197 Ok(())
198 }
199
200 pub async fn install_build_dependencies<R: Rockspec>(
201 &self,
202 build_backend: &str,
203 rocks: &R,
204 progress_arc: Arc<Progress<MultiProgress>>,
205 ) -> Result<(), InstallBuildDependenciesError> {
206 let progress = Arc::clone(&progress_arc);
207 let mut lockfile = self.tree.lockfile()?.write_guard();
208 let bar = progress.map(|p| p.new_bar());
209 let package_db = RemotePackageDB::from_config(&self.config, &bar).await?;
210 bar.map(|b| b.finish_and_clear());
211 let build_dependencies = match rocks.format() {
212 Some(RockspecFormat::_1_0 | RockspecFormat::_2_0) => {
213 rocks
216 .dependencies()
217 .current_platform()
218 .iter()
219 .filter(|dep| dep.name().to_string().contains(build_backend))
220 .cloned()
221 .collect_vec()
222 }
223 _ => rocks.build_dependencies().current_platform().to_vec(),
224 }
225 .into_iter()
226 .map(|dep| PackageInstallSpec::new(dep.package_req, tree::EntryType::Entrypoint).build())
227 .collect_vec();
228
229 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
230 get_all_dependencies(
231 tx,
232 build_dependencies,
233 Arc::new(package_db),
234 Arc::new(lockfile.clone()),
235 &self.config,
236 progress_arc,
237 )
238 .await?;
239
240 let mut all_packages = HashMap::with_capacity(rx.len());
241 while let Some(dep) = rx.recv().await {
242 all_packages.insert(dep.spec.id(), dep);
243 }
244
245 let installed_packages = join_all(all_packages.clone().into_values().map(|install_spec| {
246 let bar = progress.map(|p| {
247 p.add(ProgressBar::from(format!(
248 "💻 Installing build dependency: {}",
249 install_spec.downloaded_rock.rockspec().package(),
250 )))
251 });
252 let config = self.config.clone();
253 let tree = self.tree.clone();
254 tokio::spawn(async move {
255 let rockspec = install_spec.downloaded_rock.rockspec();
256 let pkg = Build::new(rockspec, &tree, tree::EntryType::Entrypoint, &config, &bar)
257 .constraint(install_spec.spec.constraint())
258 .behaviour(install_spec.build_behaviour)
259 .build()
260 .await?;
261
262 bar.map(|b| b.finish_and_clear());
263
264 Ok::<_, InstallBuildDependenciesError>((pkg.id(), (pkg, install_spec.entry_type)))
265 })
266 }))
267 .await
268 .into_iter()
269 .flatten()
270 .try_collect::<_, HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>, _>()?;
271
272 installed_packages
273 .iter()
274 .for_each(|(id, (pkg, entry_type))| {
275 if *entry_type == tree::EntryType::Entrypoint {
276 lockfile.add_entrypoint(pkg);
277 }
278
279 all_packages
280 .get(id)
281 .map(|pkg| pkg.spec.dependencies())
282 .unwrap_or_default()
283 .into_iter()
284 .for_each(|dependency_id| {
285 lockfile.add_dependency(
286 pkg,
287 &installed_packages
288 .get(dependency_id)
289 .expect("required dependency not found [This is a bug!]")
291 .0,
292 );
293 });
294 });
295
296 Ok(())
297 }
298
299 pub async fn make(
300 self,
301 rockspec_path: &Path,
302 build_dir: &Path,
303 dest_dir: &Path,
304 lua: &LuaInstallation,
305 ) -> Result<(), ExecLuaRocksError> {
306 std::fs::create_dir_all(dest_dir)?;
307 let dest_dir_str = dest_dir.to_slash_lossy().to_string();
308 let rockspec_path_str = rockspec_path.to_slash_lossy().to_string();
309 let args = vec![
310 "make",
311 "--deps-mode",
312 "none",
313 "--tree",
314 &dest_dir_str,
315 &rockspec_path_str,
316 ];
317 self.exec(args, build_dir, lua).await
318 }
319
320 async fn exec(
321 self,
322 args: Vec<&str>,
323 cwd: &Path,
324 lua: &LuaInstallation,
325 ) -> Result<(), ExecLuaRocksError> {
326 let luarocks_paths = Paths::new(&self.tree)?;
327 let temp_dir = TempDir::new("lux-run-luarocks").unwrap();
329 let lua_version_str = match lua.version {
330 LuaVersion::Lua51 | LuaVersion::LuaJIT => "5.1",
331 LuaVersion::Lua52 | LuaVersion::LuaJIT52 => "5.2",
332 LuaVersion::Lua53 => "5.3",
333 LuaVersion::Lua54 => "5.4",
334 };
335 let luarocks_config_content = format!(
336 r#"
337lua_version = "{0}"
338variables = {{
339 LUA_LIBDIR = "$(LUA_LIBDIR)",
340 LUA_INCDIR = "$(LUA_INCDIR)",
341 LUA_VERSION = "{1}",
342 MAKE = "{2}",
343}}
344"#,
345 lua_version_str,
346 LuaVersion::from(&self.config)?,
347 self.config.make_cmd(),
348 );
349 let luarocks_config_content =
350 variables::substitute(&[lua, &self.config], &luarocks_config_content)?;
351 let luarocks_config = temp_dir.path().join("luarocks-config.lua");
352 std::fs::write(luarocks_config.clone(), luarocks_config_content)
353 .map_err(ExecLuaRocksError::WriteLuarocksConfigError)?;
354 let luarocks_bin = self.tree.bin().join(LUAROCKS_EXE);
355 if !luarocks_bin.is_file() {
356 return Err(ExecLuaRocksError::LuarocksBinNotFound(luarocks_bin));
357 }
358 let output = Command::new(luarocks_bin)
359 .current_dir(cwd)
360 .args(args)
361 .env("PATH", luarocks_paths.path_prepended().joined())
362 .env("LUA_PATH", luarocks_paths.package_path().joined())
363 .env("LUA_CPATH", luarocks_paths.package_cpath().joined())
364 .env("HOME", temp_dir.into_path().to_slash_lossy().to_string())
365 .env(
366 "LUAROCKS_CONFIG",
367 luarocks_config.to_slash_lossy().to_string(),
368 )
369 .output()
370 .await?;
371 if output.status.success() {
372 Ok(())
373 } else {
374 Err(ExecLuaRocksError::CommandFailure {
375 status: output.status,
376 stdout: String::from_utf8_lossy(&output.stdout).into(),
377 stderr: String::from_utf8_lossy(&output.stderr).into(),
378 })
379 }
380 }
381}