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