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