Skip to main content

lux_lib/luarocks/
install_binary_rock.rs

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