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