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 crate::{
8    build::{
9        external_dependency::{ExternalDependencyError, ExternalDependencyInfo},
10        utils::recursive_copy_dir,
11        BuildBehaviour,
12    },
13    config::Config,
14    hash::HasIntegrity,
15    lockfile::{
16        LockConstraint, LockedPackage, LockedPackageHashes, LockedPackageSpec, LockfileError,
17        OptState, PinnedState,
18    },
19    lua_rockspec::{LuaVersionError, RemoteLuaRockspec},
20    luarocks::rock_manifest::RockManifest,
21    remote_package_source::RemotePackageSource,
22    rockspec::Rockspec,
23    tree::{self, InstallTree, TreeError},
24};
25use crate::{fs, lockfile::RemotePackageSourceUrl, rockspec::LuaVersionCompatibility};
26use bytes::Bytes;
27use miette::Diagnostic;
28use thiserror::Error;
29
30use super::rock_manifest::RockManifestError;
31
32#[derive(Error, Debug, Diagnostic)]
33#[non_exhaustive]
34pub enum InstallBinaryRockError {
35    #[error("IO operation failed")]
36    Io(#[from] io::Error),
37    #[error(transparent)]
38    #[diagnostic(transparent)]
39    Fs(#[from] fs::FsError),
40    #[error(transparent)]
41    #[diagnostic(transparent)]
42    Lockfile(#[from] LockfileError),
43    #[error(transparent)]
44    #[diagnostic(transparent)]
45    Tree(#[from] TreeError),
46    #[error(transparent)]
47    #[diagnostic(transparent)]
48    ExternalDependencyError(#[from] ExternalDependencyError),
49    #[error(transparent)]
50    #[diagnostic(transparent)]
51    LuaVersionError(#[from] LuaVersionError),
52    #[error("failed to unpack packed rock")]
53    Zip(#[from] zip::result::ZipError),
54    #[error(
55        "rock_manifest not found. Cannot install rock files that were packed using LuaRocks version 1"
56    )]
57    RockManifestNotFound,
58    #[error(transparent)]
59    #[diagnostic(transparent)]
60    RockManifestError(#[from] RockManifestError),
61    #[error(
62        "the entry {0} listed in the `rock_manifest` is neither a file nor a directory: {1:?}"
63    )]
64    NotAFileOrDirectory(String, Box<std::fs::Metadata>),
65}
66
67pub(crate) struct BinaryRockInstall<'a, T>
68where
69    T: InstallTree,
70{
71    rockspec: &'a RemoteLuaRockspec,
72    rock_bytes: Bytes,
73    source: RemotePackageSource,
74    pin: PinnedState,
75    opt: OptState,
76    entry_type: tree::EntryType,
77    constraint: LockConstraint,
78    behaviour: BuildBehaviour,
79    config: &'a Config,
80    tree: &'a T,
81}
82
83impl<'a, T> BinaryRockInstall<'a, T>
84where
85    T: InstallTree,
86{
87    pub(crate) fn new(
88        rockspec: &'a RemoteLuaRockspec,
89        source: RemotePackageSource,
90        rock_bytes: Bytes,
91        entry_type: tree::EntryType,
92        config: &'a Config,
93        tree: &'a T,
94    ) -> Self {
95        Self {
96            rockspec,
97            rock_bytes,
98            source,
99            config,
100            tree,
101            constraint: LockConstraint::default(),
102            behaviour: BuildBehaviour::Ignore,
103            pin: PinnedState::default(),
104            opt: OptState::default(),
105            entry_type,
106        }
107    }
108
109    pub(crate) fn pin(self, pin: PinnedState) -> Self {
110        Self { pin, ..self }
111    }
112
113    pub(crate) fn opt(self, opt: OptState) -> Self {
114        Self { opt, ..self }
115    }
116
117    pub(crate) fn constraint(self, constraint: LockConstraint) -> Self {
118        Self { constraint, ..self }
119    }
120
121    pub(crate) fn behaviour(self, behaviour: BuildBehaviour) -> Self {
122        Self { behaviour, ..self }
123    }
124
125    #[tracing::instrument(name = "Installing binary rock", skip_all)]
126    pub(crate) async fn install(self) -> Result<LockedPackage, InstallBinaryRockError> {
127        let rockspec = self.rockspec;
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 = LockedPackageHashes {
135            rockspec: rockspec.hash().await?,
136            source: self.rock_bytes.hash().await?,
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 spec = LockedPackageSpec::new(
145            rockspec.package(),
146            rockspec.version(),
147            self.constraint,
148            Vec::new(),
149            Vec::new(),
150            &self.pin,
151            &self.opt,
152        );
153        let package = LockedPackage::new(spec, self.source, source_url, hashes);
154        match self.tree.lockfile()?.get(&package.id()) {
155            Some(package)
156                if matches!(
157                    self.behaviour,
158                    BuildBehaviour::Ignore | BuildBehaviour::Conflict
159                ) =>
160            {
161                Ok(package.clone())
162            }
163            _ => {
164                let unpack_dir = fs::tempfile::tempdir()?;
165                let cursor = Cursor::new(self.rock_bytes);
166                let mut zip = zip::ZipArchive::new(cursor)?;
167                zip.extract(&unpack_dir)?;
168                // let lua_dir = unpack_dir.join("lua");
169                // if lua_dir.is_dir() {
170                //     let src_dir = unpack_dir.join("lua");
171                //     tokio::fs::rename(lua_dir, src_dir).await?;
172                // }
173                let rock_manifest_file = unpack_dir.path().join("rock_manifest");
174                if !rock_manifest_file.is_file() {
175                    return Err(InstallBinaryRockError::RockManifestNotFound);
176                }
177                let rock_manifest_content = fs::tokio::read_to_string(rock_manifest_file).await?;
178                self.tree.prepare(&package.spec)?;
179                let layout = self.tree.layout_for(&package.spec);
180                let rock_manifest = RockManifest::new(&rock_manifest_content)?;
181                install_manifest_entries(
182                    &rock_manifest.lib.entries,
183                    &unpack_dir.path().join("lib"),
184                    &layout.lib,
185                )
186                .await?;
187                install_manifest_entries(
188                    &rock_manifest.lua.entries,
189                    &unpack_dir.path().join("lua"),
190                    &layout.src,
191                )
192                .await?;
193                install_bin_manifest_entries(
194                    &rock_manifest.bin.entries,
195                    &unpack_dir.path().join("bin"),
196                    &self.tree.bin(),
197                )
198                .await?;
199                install_manifest_entries(
200                    &rock_manifest.doc.entries,
201                    &unpack_dir.path().join("doc"),
202                    &layout.doc,
203                )
204                .await?;
205                install_manifest_entries(
206                    &rock_manifest.root.entries,
207                    unpack_dir.path(),
208                    &layout.etc,
209                )
210                .await?;
211                // rename <name>-<version>.rockspec
212                let rockspec_path =
213                    layout
214                        .etc
215                        .join(format!("{}-{}.rockspec", package.name(), package.version()));
216                if rockspec_path.is_file() {
217                    fs::tokio::copy(&rockspec_path, layout.rockspec_path()).await?;
218                    fs::tokio::remove_file(&rockspec_path).await?;
219                }
220                self.tree.finalize(&package, self.entry_type)?;
221                self.tree
222                    .lockfile()?
223                    .write_guard()
224                    .set_binaries(&package, rockspec.binaries());
225                Ok(package)
226            }
227        }
228    }
229}
230
231#[tracing::instrument(level = "trace", skip(entry))]
232async fn install_manifest_entries<T>(
233    entry: &HashMap<PathBuf, T>,
234    src: &Path,
235    dest: &Path,
236) -> Result<(), InstallBinaryRockError> {
237    for relative_src_path in entry.keys() {
238        let target = dest.join(relative_src_path);
239        let src_path = src.join(relative_src_path);
240        if src_path.is_dir() {
241            recursive_copy_dir(&src_path, &target).await?;
242        } else if src_path.is_file() {
243            if let Some(target_parent_dir) = target.parent() {
244                fs::tokio::create_dir_all(target_parent_dir).await?;
245            }
246            fs::tokio::copy(src.join(relative_src_path), target).await?;
247        } else {
248            let metadata = fs::tokio::metadata(&src_path).await?;
249            return Err(InstallBinaryRockError::NotAFileOrDirectory(
250                src_path.to_string_lossy().to_string(),
251                Box::new(metadata),
252            ));
253        }
254    }
255    Ok(())
256}
257
258async fn install_bin_manifest_entries(
259    entries: &HashMap<PathBuf, String>,
260    src: &Path,
261    dest: &Path,
262) -> Result<(), InstallBinaryRockError> {
263    install_manifest_entries(entries, src, dest).await?;
264    #[cfg(unix)]
265    for relative_src_path in entries.keys() {
266        crate::build::utils::set_executable_permissions(&dest.join(relative_src_path)).await?;
267    }
268    Ok(())
269}
270
271#[cfg(test)]
272mod tests {
273
274    use assert_fs::TempDir;
275    use io::{Read, Write};
276    use is_executable::IsExecutable;
277    use zip::write::SimpleFileOptions;
278
279    use crate::{
280        config::ConfigBuilder,
281        operations::{unpack_rockspec, DownloadedPackedRockBytes},
282    };
283
284    use super::*;
285    #[cfg(feature = "impure_tests")]
286    use flaky_test::flaky_test;
287
288    #[tokio::test]
289    async fn install_binary_rock() {
290        let rock = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
291            .join("resources/test/sample-project-0.1.0-1.all.rock");
292        let content = std::fs::read(rock).unwrap();
293        let rock_bytes = Bytes::copy_from_slice(&content);
294        let packed_rock_file_name = "sample-project-0.1.0-1.all.rock".to_string();
295        let cursor = Cursor::new(rock_bytes.clone());
296        let mut zip = zip::ZipArchive::new(cursor).unwrap();
297        let manifest_index = zip.index_for_path("rock_manifest").unwrap();
298        let mut manifest_file = zip.by_index(manifest_index).unwrap();
299        let mut content = String::new();
300        manifest_file.read_to_string(&mut content).unwrap();
301        let rock = DownloadedPackedRockBytes {
302            name: "sample-project".into(),
303            version: "0.1.0-1".parse().unwrap(),
304            bytes: rock_bytes,
305            file_name: packed_rock_file_name.clone(),
306            url: "https://test.org".parse().unwrap(),
307        };
308        let rockspec = unpack_rockspec(&rock).await.unwrap();
309        let install_root = assert_fs::TempDir::new().unwrap();
310        let config = ConfigBuilder::new()
311            .unwrap()
312            .user_tree(Some(install_root.to_path_buf()))
313            .build()
314            .unwrap();
315        let tree = config
316            .user_tree(config.lua_version().unwrap().clone())
317            .unwrap();
318        let local_package = BinaryRockInstall::new(
319            &rockspec,
320            RemotePackageSource::Test,
321            rock.bytes,
322            tree::EntryType::Entrypoint,
323            &config,
324            &tree,
325        )
326        .install()
327        .await
328        .unwrap();
329        assert_eq!(
330            local_package.hashes().source,
331            "sha256-zDmnjE2b8GxX+6j5vTCWUEBxe0aFXFVhC4JOrnAgQ2g="
332                .parse()
333                .unwrap()
334        );
335        let foo_bar_module = tree
336            .layout_for(&local_package.spec)
337            .src
338            .join("foo")
339            .join("bar.lua");
340        assert!(foo_bar_module.is_file());
341    }
342
343    fn packed_rock_with_bin() -> Vec<u8> {
344        let rockspec = r#"
345rockspec_format = "3.0"
346package = "test-binary-bin"
347version = "1.0-1"
348source = { url = "https://example.com/test-binary-bin.zip" }
349"#;
350        let manifest = r#"rock_manifest = { bin = { mytool = "0" } }"#;
351        let mut buf = Vec::new();
352        {
353            let mut zip = zip::ZipWriter::new(Cursor::new(&mut buf));
354            let options = SimpleFileOptions::default();
355            for (name, content) in [
356                ("test-binary-bin-1.0-1.rockspec", rockspec.as_bytes()),
357                ("bin/mytool", b"#!/bin/sh\necho hi\n".as_slice()),
358                ("rock_manifest", manifest.as_bytes()),
359            ] {
360                zip.start_file(name, options).unwrap();
361                zip.write_all(content).unwrap();
362            }
363            zip.finish().unwrap();
364        }
365        buf
366    }
367
368    #[cfg(unix)]
369    #[tokio::test]
370    async fn install_binary_rock_makes_bin_executable() {
371        let rock = DownloadedPackedRockBytes {
372            name: "test-binary-bin".into(),
373            version: "1.0-1".parse().unwrap(),
374            bytes: Bytes::from(packed_rock_with_bin()),
375            file_name: "test-binary-bin-1.0-1.all.rock".into(),
376            url: "https://test.org".parse().unwrap(),
377        };
378        let rockspec = unpack_rockspec(&rock).await.unwrap();
379        let install_root = TempDir::new().unwrap();
380        let config = ConfigBuilder::new()
381            .unwrap()
382            .user_tree(Some(install_root.to_path_buf()))
383            .build()
384            .unwrap();
385        let tree = config
386            .user_tree(config.lua_version().unwrap().clone())
387            .unwrap();
388        BinaryRockInstall::new(
389            &rockspec,
390            RemotePackageSource::Test,
391            rock.bytes,
392            tree::EntryType::Entrypoint,
393            &config,
394            &tree,
395        )
396        .install()
397        .await
398        .unwrap();
399        assert!(tree.bin().join("mytool").is_executable());
400    }
401
402    /// This relatively large integration test case tests the following:
403    ///
404    /// - Install a packed rock that was packed using luarocks 3.11 from the test resources.
405    /// - Pack the rock using our own `Pack` implementation.
406    /// - Verify that the `rock_manifest` entry of the original packed rock and our own packed rock
407    ///   are equal (this means luarocks should be able to install our packed rock).
408    /// - Uninstall the local package.
409    /// - Install the package from our packed rock.
410    /// - Verify that the contents of the install directories when installing from both packed rocks
411    ///   are the same.
412    #[cfg(all(target_os = "linux", target_arch = "x86_64", feature = "impure_tests"))]
413    #[flaky_test(tokio, times = 5)]
414    async fn install_binary_rock_roundtrip() {
415        use crate::operations::{Pack, Uninstall};
416
417        let rock = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
418            .join("resources/test/toml-edit-0.6.0-1.linux-x86_64.rock");
419        let content = std::fs::read(rock).unwrap();
420        let rock_bytes = Bytes::copy_from_slice(&content);
421        let packed_rock_file_name = "toml-edit-0.6.0-1.linux-x86_64.rock".to_string();
422        let cursor = Cursor::new(rock_bytes.clone());
423        let mut zip = zip::ZipArchive::new(cursor).unwrap();
424        let manifest_index = zip.index_for_path("rock_manifest").unwrap();
425        let mut manifest_file = zip.by_index(manifest_index).unwrap();
426        let mut content = String::new();
427        manifest_file.read_to_string(&mut content).unwrap();
428        let orig_manifest = RockManifest::new(&content).unwrap();
429        let rock = DownloadedPackedRockBytes {
430            name: "toml-edit".into(),
431            version: "0.6.0-1".parse().unwrap(),
432            bytes: rock_bytes,
433            file_name: packed_rock_file_name.clone(),
434            url: "https://test.org".parse().unwrap(),
435        };
436        let rockspec = unpack_rockspec(&rock).await.unwrap();
437        let install_root = assert_fs::TempDir::new().unwrap();
438        let config = ConfigBuilder::new()
439            .unwrap()
440            .user_tree(Some(install_root.to_path_buf()))
441            .build()
442            .unwrap();
443        let tree = config
444            .user_tree(config.lua_version().unwrap().clone())
445            .unwrap();
446        let local_package = BinaryRockInstall::new(
447            &rockspec,
448            RemotePackageSource::Test,
449            rock.bytes,
450            tree::EntryType::Entrypoint,
451            &config,
452            &tree,
453        )
454        .install()
455        .await
456        .unwrap();
457        let layout = tree.layout_for(local_package.spec());
458        assert!(layout.lib.join("toml_edit.so").is_file());
459
460        let orig_install_tree_integrity = layout.root.hash().await.unwrap();
461
462        let pack_dest_dir = assert_fs::TempDir::new().unwrap();
463        let packed_rock = Pack::new(
464            pack_dest_dir.to_path_buf(),
465            tree.clone(),
466            local_package.clone(),
467        )
468        .pack()
469        .await
470        .unwrap();
471        assert_eq!(
472            packed_rock
473                .file_name()
474                .unwrap()
475                .to_string_lossy()
476                .to_string(),
477            packed_rock_file_name.clone()
478        );
479
480        // let's make sure our own pack/unpack implementation roundtrips correctly
481        Uninstall::new()
482            .config(&config)
483            .package(local_package.id())
484            .remove()
485            .await
486            .unwrap();
487        let content = std::fs::read(&packed_rock).unwrap();
488        let rock_bytes = Bytes::copy_from_slice(&content);
489        let cursor = Cursor::new(rock_bytes.clone());
490        let mut zip = zip::ZipArchive::new(cursor).unwrap();
491        let manifest_index = zip.index_for_path("rock_manifest").unwrap();
492        let mut manifest_file = zip.by_index(manifest_index).unwrap();
493        let mut content = String::new();
494        manifest_file.read_to_string(&mut content).unwrap();
495        let packed_manifest = RockManifest::new(&content).unwrap();
496        assert_eq!(packed_manifest, orig_manifest);
497        let rock = DownloadedPackedRockBytes {
498            name: "toml-edit".into(),
499            version: "0.6.0-1".parse().unwrap(),
500            bytes: rock_bytes,
501            file_name: packed_rock_file_name.clone(),
502            url: "https://test.org".parse().unwrap(),
503        };
504        let rockspec = unpack_rockspec(&rock).await.unwrap();
505        let local_package = BinaryRockInstall::new(
506            &rockspec,
507            RemotePackageSource::Test,
508            rock.bytes,
509            tree::EntryType::Entrypoint,
510            &config,
511            &tree,
512        )
513        .install()
514        .await
515        .unwrap();
516        let layout = tree.layout_for(local_package.spec());
517        assert!(layout.rockspec_path().is_file());
518        let new_install_tree_integrity = layout.root.hash().await.unwrap();
519        assert_eq!(orig_install_tree_integrity, new_install_tree_integrity);
520    }
521}