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 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 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 #[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 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}