Skip to main content

lux_lib/luarocks/
install_binary_rock.rs

1use std::{
2    collections::HashMap,
3    io::{self, Cursor},
4    path::{Path, PathBuf},
5};
6
7use bytes::Bytes;
8use tempfile::tempdir;
9use thiserror::Error;
10
11use crate::{
12    build::{
13        external_dependency::{ExternalDependencyError, ExternalDependencyInfo},
14        utils::recursive_copy_dir,
15        BuildBehaviour,
16    },
17    config::Config,
18    hash::HasIntegrity,
19    lockfile::{
20        LocalPackage, LocalPackageHashes, LockConstraint, LockfileError, OptState, PinnedState,
21    },
22    lua_rockspec::{LuaVersionError, RemoteLuaRockspec},
23    luarocks::rock_manifest::RockManifest,
24    package::PackageSpec,
25    progress::{Progress, ProgressBar},
26    remote_package_source::RemotePackageSource,
27    rockspec::Rockspec,
28    tree::{self, Tree, TreeError},
29};
30use crate::{lockfile::RemotePackageSourceUrl, rockspec::LuaVersionCompatibility};
31
32use super::rock_manifest::RockManifestError;
33
34#[derive(Error, Debug)]
35pub enum InstallBinaryRockError {
36    #[error("IO operation failed: {0}")]
37    Io(#[from] io::Error),
38    #[error(transparent)]
39    Lockfile(#[from] LockfileError),
40    #[error(transparent)]
41    Tree(#[from] TreeError),
42    #[error(transparent)]
43    ExternalDependencyError(#[from] ExternalDependencyError),
44    #[error(transparent)]
45    LuaVersionError(#[from] LuaVersionError),
46    #[error("failed to unpack packed rock: {0}")]
47    Zip(#[from] zip::result::ZipError),
48    #[error("rock_manifest not found. Cannot install rock files that were packed using LuaRocks version 1")]
49    RockManifestNotFound,
50    #[error(transparent)]
51    RockManifestError(#[from] RockManifestError),
52    #[error(
53        "the entry {0} listed in the `rock_manifest` is neither a file nor a directory: {1:?}"
54    )]
55    NotAFileOrDirectory(String, std::fs::Metadata),
56}
57
58pub(crate) struct BinaryRockInstall<'a> {
59    rockspec: &'a RemoteLuaRockspec,
60    rock_bytes: Bytes,
61    source: RemotePackageSource,
62    pin: PinnedState,
63    opt: OptState,
64    entry_type: tree::EntryType,
65    constraint: LockConstraint,
66    behaviour: BuildBehaviour,
67    config: &'a Config,
68    tree: &'a Tree,
69    progress: &'a Progress<ProgressBar>,
70}
71
72impl<'a> BinaryRockInstall<'a> {
73    pub(crate) fn new(
74        rockspec: &'a RemoteLuaRockspec,
75        source: RemotePackageSource,
76        rock_bytes: Bytes,
77        entry_type: tree::EntryType,
78        config: &'a Config,
79        tree: &'a Tree,
80        progress: &'a Progress<ProgressBar>,
81    ) -> Self {
82        Self {
83            rockspec,
84            rock_bytes,
85            source,
86            config,
87            tree,
88            progress,
89            constraint: LockConstraint::default(),
90            behaviour: BuildBehaviour::default(),
91            pin: PinnedState::default(),
92            opt: OptState::default(),
93            entry_type,
94        }
95    }
96
97    pub(crate) fn pin(self, pin: PinnedState) -> Self {
98        Self { pin, ..self }
99    }
100
101    pub(crate) fn opt(self, opt: OptState) -> Self {
102        Self { opt, ..self }
103    }
104
105    pub(crate) fn constraint(self, constraint: LockConstraint) -> Self {
106        Self { constraint, ..self }
107    }
108
109    pub(crate) fn behaviour(self, behaviour: BuildBehaviour) -> Self {
110        Self { behaviour, ..self }
111    }
112
113    pub(crate) async fn install(self) -> Result<LocalPackage, InstallBinaryRockError> {
114        let rockspec = self.rockspec;
115        self.progress.map(|p| {
116            p.set_message(format!(
117                "Unpacking and installing {}@{}...",
118                rockspec.package(),
119                rockspec.version()
120            ))
121        });
122        for (name, dep) in rockspec.external_dependencies().current_platform() {
123            let _ = ExternalDependencyInfo::probe(name, dep, self.config.external_deps())?;
124        }
125
126        rockspec.validate_lua_version_from_config(self.config)?;
127
128        let hashes = LocalPackageHashes {
129            rockspec: rockspec.hash()?,
130            source: self.rock_bytes.hash()?,
131        };
132        let source_url = match &self.source {
133            RemotePackageSource::LuarocksBinaryRock(url) => {
134                Some(RemotePackageSourceUrl::Url { url: url.clone() })
135            }
136            _ => None,
137        };
138        let mut package = LocalPackage::from(
139            &PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
140            self.constraint,
141            rockspec.binaries(),
142            self.source,
143            source_url,
144            hashes,
145        );
146        package.spec.pinned = self.pin;
147        package.spec.opt = self.opt;
148        match self.tree.lockfile()?.get(&package.id()) {
149            Some(package) if self.behaviour == BuildBehaviour::NoForce => Ok(package.clone()),
150            _ => {
151                let unpack_dir = tempdir()?;
152                let cursor = Cursor::new(self.rock_bytes);
153                let mut zip = zip::ZipArchive::new(cursor)?;
154                zip.extract(&unpack_dir)?;
155                // let lua_dir = unpack_dir.join("lua");
156                // if lua_dir.is_dir() {
157                //     let src_dir = unpack_dir.join("lua");
158                //     tokio::fs::rename(lua_dir, src_dir).await?;
159                // }
160                let rock_manifest_file = unpack_dir.path().join("rock_manifest");
161                if !rock_manifest_file.is_file() {
162                    return Err(InstallBinaryRockError::RockManifestNotFound);
163                }
164                let rock_manifest_content = tokio::fs::read_to_string(rock_manifest_file).await?;
165                let output_paths = match self.entry_type {
166                    tree::EntryType::Entrypoint => self.tree.entrypoint(&package)?,
167                    tree::EntryType::DependencyOnly => self.tree.dependency(&package)?,
168                };
169                let rock_manifest = RockManifest::new(&rock_manifest_content)?;
170                install_manifest_entries(
171                    &rock_manifest.lib.entries,
172                    &unpack_dir.path().join("lib"),
173                    &output_paths.lib,
174                )
175                .await?;
176                install_manifest_entries(
177                    &rock_manifest.lua.entries,
178                    &unpack_dir.path().join("lua"),
179                    &output_paths.src,
180                )
181                .await?;
182                install_manifest_entries(
183                    &rock_manifest.bin.entries,
184                    &unpack_dir.path().join("bin"),
185                    &output_paths.bin,
186                )
187                .await?;
188                install_manifest_entries(
189                    &rock_manifest.doc.entries,
190                    &unpack_dir.path().join("doc"),
191                    &output_paths.doc,
192                )
193                .await?;
194                install_manifest_entries(
195                    &rock_manifest.root.entries,
196                    unpack_dir.path(),
197                    &output_paths.etc,
198                )
199                .await?;
200                // rename <name>-<version>.rockspec
201                let rockspec_path = output_paths.etc.join(format!(
202                    "{}-{}.rockspec",
203                    package.name(),
204                    package.version()
205                ));
206                if rockspec_path.is_file() {
207                    tokio::fs::copy(&rockspec_path, output_paths.rockspec_path()).await?;
208                    tokio::fs::remove_file(&rockspec_path).await?;
209                }
210                Ok(package)
211            }
212        }
213    }
214}
215
216async fn install_manifest_entries<T>(
217    entry: &HashMap<PathBuf, T>,
218    src: &Path,
219    dest: &Path,
220) -> Result<(), InstallBinaryRockError> {
221    for relative_src_path in entry.keys() {
222        let target = dest.join(relative_src_path);
223        let src_path = src.join(relative_src_path);
224        if src_path.is_dir() {
225            recursive_copy_dir(&src_path, &target).await?;
226        } else if src_path.is_file() {
227            if let Some(target_parent_dir) = target.parent() {
228                tokio::fs::create_dir_all(target_parent_dir).await?;
229            }
230            tokio::fs::copy(src.join(relative_src_path), target).await?;
231        } else {
232            let metadata = tokio::fs::metadata(&src_path).await?;
233            return Err(InstallBinaryRockError::NotAFileOrDirectory(
234                src_path.to_string_lossy().to_string(),
235                metadata,
236            ));
237        }
238    }
239    Ok(())
240}
241
242#[cfg(test)]
243mod test {
244
245    use io::Read;
246
247    use crate::{
248        config::ConfigBuilder,
249        operations::{unpack_rockspec, DownloadedPackedRockBytes},
250        progress::MultiProgress,
251    };
252
253    use super::*;
254
255    #[tokio::test]
256    async fn install_binary_rock() {
257        let content = std::fs::read("resources/test/sample-project-0.1.0-1.all.rock").unwrap();
258        let rock_bytes = Bytes::copy_from_slice(&content);
259        let packed_rock_file_name = "sample-project-0.1.0-1.all.rock".to_string();
260        let cursor = Cursor::new(rock_bytes.clone());
261        let mut zip = zip::ZipArchive::new(cursor).unwrap();
262        let manifest_index = zip.index_for_path("rock_manifest").unwrap();
263        let mut manifest_file = zip.by_index(manifest_index).unwrap();
264        let mut content = String::new();
265        manifest_file.read_to_string(&mut content).unwrap();
266        let rock = DownloadedPackedRockBytes {
267            name: "sample-project".into(),
268            version: "0.1.0-1".parse().unwrap(),
269            bytes: rock_bytes,
270            file_name: packed_rock_file_name.clone(),
271            url: "https://test.org".parse().unwrap(),
272        };
273        let rockspec = unpack_rockspec(&rock).await.unwrap();
274        let install_root = assert_fs::TempDir::new().unwrap();
275        let config = ConfigBuilder::new()
276            .unwrap()
277            .user_tree(Some(install_root.to_path_buf()))
278            .build()
279            .unwrap();
280        let progress = MultiProgress::new(&config);
281        let bar = progress.map(MultiProgress::new_bar);
282        let tree = config
283            .user_tree(config.lua_version().unwrap().clone())
284            .unwrap();
285        let local_package = BinaryRockInstall::new(
286            &rockspec,
287            RemotePackageSource::Test,
288            rock.bytes,
289            tree::EntryType::Entrypoint,
290            &config,
291            &tree,
292            &bar,
293        )
294        .install()
295        .await
296        .unwrap();
297        let rock_layout = tree.entrypoint_layout(&local_package);
298        let foo_bar_module = rock_layout.src.join("foo").join("bar.lua");
299        assert!(foo_bar_module.is_file());
300    }
301
302    /// This relatively large integration test case tests the following:
303    ///
304    /// - Install a packed rock that was packed using luarocks 3.11 from the test resources.
305    /// - Pack the rock using our own `Pack` implementation.
306    /// - Verify that the `rock_manifest` entry of the original packed rock and our own packed rock
307    ///   are equal (this means luarocks should be able to install our packed rock).
308    /// - Uninstall the local package.
309    /// - Install the package from our packed rock.
310    /// - Verify that the contents of the install directories when installing from both packed rocks
311    ///   are the same.
312    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
313    #[tokio::test]
314    async fn install_binary_rock_roundtrip() {
315        use crate::operations::{Pack, Uninstall};
316
317        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
318            println!("Skipping impure test");
319            return;
320        }
321        let content = std::fs::read("resources/test/toml-edit-0.6.0-1.linux-x86_64.rock").unwrap();
322        let rock_bytes = Bytes::copy_from_slice(&content);
323        let packed_rock_file_name = "toml-edit-0.6.0-1.linux-x86_64.rock".to_string();
324        let cursor = Cursor::new(rock_bytes.clone());
325        let mut zip = zip::ZipArchive::new(cursor).unwrap();
326        let manifest_index = zip.index_for_path("rock_manifest").unwrap();
327        let mut manifest_file = zip.by_index(manifest_index).unwrap();
328        let mut content = String::new();
329        manifest_file.read_to_string(&mut content).unwrap();
330        let orig_manifest = RockManifest::new(&content).unwrap();
331        let rock = DownloadedPackedRockBytes {
332            name: "toml-edit".into(),
333            version: "0.6.0-1".parse().unwrap(),
334            bytes: rock_bytes,
335            file_name: packed_rock_file_name.clone(),
336            url: "https://test.org".parse().unwrap(),
337        };
338        let rockspec = unpack_rockspec(&rock).await.unwrap();
339        let install_root = assert_fs::TempDir::new().unwrap();
340        let config = ConfigBuilder::new()
341            .unwrap()
342            .user_tree(Some(install_root.to_path_buf()))
343            .build()
344            .unwrap();
345        let progress = MultiProgress::new(&config);
346        let bar = progress.map(MultiProgress::new_bar);
347        let tree = config
348            .user_tree(config.lua_version().unwrap().clone())
349            .unwrap();
350        let local_package = BinaryRockInstall::new(
351            &rockspec,
352            RemotePackageSource::Test,
353            rock.bytes,
354            tree::EntryType::Entrypoint,
355            &config,
356            &tree,
357            &bar,
358        )
359        .install()
360        .await
361        .unwrap();
362        let rock_layout = tree.entrypoint_layout(&local_package);
363
364        assert!(rock_layout.lib.join("toml_edit.so").is_file());
365
366        let orig_install_tree_integrity = rock_layout.rock_path.hash().unwrap();
367
368        let pack_dest_dir = assert_fs::TempDir::new().unwrap();
369        let packed_rock = Pack::new(
370            pack_dest_dir.to_path_buf(),
371            tree.clone(),
372            local_package.clone(),
373        )
374        .pack()
375        .await
376        .unwrap();
377        assert_eq!(
378            packed_rock
379                .file_name()
380                .unwrap()
381                .to_string_lossy()
382                .to_string(),
383            packed_rock_file_name.clone()
384        );
385
386        // let's make sure our own pack/unpack implementation roundtrips correctly
387        Uninstall::new()
388            .config(&config)
389            .package(local_package.id())
390            .remove()
391            .await
392            .unwrap();
393        let content = std::fs::read(&packed_rock).unwrap();
394        let rock_bytes = Bytes::copy_from_slice(&content);
395        let cursor = Cursor::new(rock_bytes.clone());
396        let mut zip = zip::ZipArchive::new(cursor).unwrap();
397        let manifest_index = zip.index_for_path("rock_manifest").unwrap();
398        let mut manifest_file = zip.by_index(manifest_index).unwrap();
399        let mut content = String::new();
400        manifest_file.read_to_string(&mut content).unwrap();
401        let packed_manifest = RockManifest::new(&content).unwrap();
402        assert_eq!(packed_manifest, orig_manifest);
403        let rock = DownloadedPackedRockBytes {
404            name: "toml-edit".into(),
405            version: "0.6.0-1".parse().unwrap(),
406            bytes: rock_bytes,
407            file_name: packed_rock_file_name.clone(),
408            url: "https://test.org".parse().unwrap(),
409        };
410        let rockspec = unpack_rockspec(&rock).await.unwrap();
411        let local_package = BinaryRockInstall::new(
412            &rockspec,
413            RemotePackageSource::Test,
414            rock.bytes,
415            tree::EntryType::Entrypoint,
416            &config,
417            &tree,
418            &bar,
419        )
420        .install()
421        .await
422        .unwrap();
423        let rock_layout = tree.entrypoint_layout(&local_package);
424        assert!(rock_layout.rockspec_path().is_file());
425        let new_install_tree_integrity = rock_layout.rock_path.hash().unwrap();
426        assert_eq!(orig_install_tree_integrity, new_install_tree_integrity);
427    }
428}