Skip to main content

lux_lib/luarocks/
install_binary_rock.rs

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