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