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_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
258#[cfg(test)]
259mod tests {
260
261 use io::Read;
262
263 use crate::{
264 config::ConfigBuilder,
265 operations::{unpack_rockspec, DownloadedPackedRockBytes},
266 };
267
268 use super::*;
269 #[cfg(feature = "impure_tests")]
270 use flaky_test::flaky_test;
271
272 #[tokio::test]
273 async fn install_binary_rock() {
274 let rock = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
275 .join("resources/test/sample-project-0.1.0-1.all.rock");
276 let content = std::fs::read(rock).unwrap();
277 let rock_bytes = Bytes::copy_from_slice(&content);
278 let packed_rock_file_name = "sample-project-0.1.0-1.all.rock".to_string();
279 let cursor = Cursor::new(rock_bytes.clone());
280 let mut zip = zip::ZipArchive::new(cursor).unwrap();
281 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
282 let mut manifest_file = zip.by_index(manifest_index).unwrap();
283 let mut content = String::new();
284 manifest_file.read_to_string(&mut content).unwrap();
285 let rock = DownloadedPackedRockBytes {
286 name: "sample-project".into(),
287 version: "0.1.0-1".parse().unwrap(),
288 bytes: rock_bytes,
289 file_name: packed_rock_file_name.clone(),
290 url: "https://test.org".parse().unwrap(),
291 };
292 let rockspec = unpack_rockspec(&rock).await.unwrap();
293 let install_root = assert_fs::TempDir::new().unwrap();
294 let config = ConfigBuilder::new()
295 .unwrap()
296 .user_tree(Some(install_root.to_path_buf()))
297 .build()
298 .unwrap();
299 let tree = config
300 .user_tree(config.lua_version().unwrap().clone())
301 .unwrap();
302 let local_package = BinaryRockInstall::new(
303 &rockspec,
304 RemotePackageSource::Test,
305 rock.bytes,
306 tree::EntryType::Entrypoint,
307 &config,
308 &tree,
309 )
310 .install()
311 .await
312 .unwrap();
313 assert_eq!(
314 local_package.hashes().source,
315 "sha256-zDmnjE2b8GxX+6j5vTCWUEBxe0aFXFVhC4JOrnAgQ2g="
316 .parse()
317 .unwrap()
318 );
319 let foo_bar_module = tree
320 .layout_for(&local_package.spec)
321 .src
322 .join("foo")
323 .join("bar.lua");
324 assert!(foo_bar_module.is_file());
325 }
326
327 #[cfg(all(target_os = "linux", target_arch = "x86_64", feature = "impure_tests"))]
338 #[flaky_test(tokio, times = 5)]
339 async fn install_binary_rock_roundtrip() {
340 use crate::operations::{Pack, Uninstall};
341
342 let rock = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
343 .join("resources/test/toml-edit-0.6.0-1.linux-x86_64.rock");
344 let content = std::fs::read(rock).unwrap();
345 let rock_bytes = Bytes::copy_from_slice(&content);
346 let packed_rock_file_name = "toml-edit-0.6.0-1.linux-x86_64.rock".to_string();
347 let cursor = Cursor::new(rock_bytes.clone());
348 let mut zip = zip::ZipArchive::new(cursor).unwrap();
349 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
350 let mut manifest_file = zip.by_index(manifest_index).unwrap();
351 let mut content = String::new();
352 manifest_file.read_to_string(&mut content).unwrap();
353 let orig_manifest = RockManifest::new(&content).unwrap();
354 let rock = DownloadedPackedRockBytes {
355 name: "toml-edit".into(),
356 version: "0.6.0-1".parse().unwrap(),
357 bytes: rock_bytes,
358 file_name: packed_rock_file_name.clone(),
359 url: "https://test.org".parse().unwrap(),
360 };
361 let rockspec = unpack_rockspec(&rock).await.unwrap();
362 let install_root = assert_fs::TempDir::new().unwrap();
363 let config = ConfigBuilder::new()
364 .unwrap()
365 .user_tree(Some(install_root.to_path_buf()))
366 .build()
367 .unwrap();
368 let tree = config
369 .user_tree(config.lua_version().unwrap().clone())
370 .unwrap();
371 let local_package = BinaryRockInstall::new(
372 &rockspec,
373 RemotePackageSource::Test,
374 rock.bytes,
375 tree::EntryType::Entrypoint,
376 &config,
377 &tree,
378 )
379 .install()
380 .await
381 .unwrap();
382 let layout = tree.layout_for(local_package.spec());
383 assert!(layout.lib.join("toml_edit.so").is_file());
384
385 let orig_install_tree_integrity = layout.root.hash().await.unwrap();
386
387 let pack_dest_dir = assert_fs::TempDir::new().unwrap();
388 let packed_rock = Pack::new(
389 pack_dest_dir.to_path_buf(),
390 tree.clone(),
391 local_package.clone(),
392 )
393 .pack()
394 .await
395 .unwrap();
396 assert_eq!(
397 packed_rock
398 .file_name()
399 .unwrap()
400 .to_string_lossy()
401 .to_string(),
402 packed_rock_file_name.clone()
403 );
404
405 Uninstall::new()
407 .config(&config)
408 .package(local_package.id())
409 .remove()
410 .await
411 .unwrap();
412 let content = std::fs::read(&packed_rock).unwrap();
413 let rock_bytes = Bytes::copy_from_slice(&content);
414 let cursor = Cursor::new(rock_bytes.clone());
415 let mut zip = zip::ZipArchive::new(cursor).unwrap();
416 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
417 let mut manifest_file = zip.by_index(manifest_index).unwrap();
418 let mut content = String::new();
419 manifest_file.read_to_string(&mut content).unwrap();
420 let packed_manifest = RockManifest::new(&content).unwrap();
421 assert_eq!(packed_manifest, orig_manifest);
422 let rock = DownloadedPackedRockBytes {
423 name: "toml-edit".into(),
424 version: "0.6.0-1".parse().unwrap(),
425 bytes: rock_bytes,
426 file_name: packed_rock_file_name.clone(),
427 url: "https://test.org".parse().unwrap(),
428 };
429 let rockspec = unpack_rockspec(&rock).await.unwrap();
430 let local_package = BinaryRockInstall::new(
431 &rockspec,
432 RemotePackageSource::Test,
433 rock.bytes,
434 tree::EntryType::Entrypoint,
435 &config,
436 &tree,
437 )
438 .install()
439 .await
440 .unwrap();
441 let layout = tree.layout_for(local_package.spec());
442 assert!(layout.rockspec_path().is_file());
443 let new_install_tree_integrity = layout.root.hash().await.unwrap();
444 assert_eq!(orig_install_tree_integrity, new_install_tree_integrity);
445 }
446}