lux_lib/luarocks/
luarocks_installation.rs

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)]
57    // Io(#[from] io::Error),
58    #[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                // XXX: rockspec formats < 3.0 don't support `build_dependencies`,
214                // so we have to fetch the build backend from the dependencies.
215                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                                // NOTE: This can happen if an install thread panics
290                                .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        // Ensure a pure environment so we can do parallel builds
328        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}