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, Tree, 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> {
59 rockspec: &'a RemoteLuaRockspec,
60 rock_bytes: Bytes,
61 source: RemotePackageSource,
62 pin: PinnedState,
63 opt: OptState,
64 entry_type: tree::EntryType,
65 constraint: LockConstraint,
66 behaviour: BuildBehaviour,
67 config: &'a Config,
68 tree: &'a Tree,
69 progress: &'a Progress<ProgressBar>,
70}
71
72impl<'a> BinaryRockInstall<'a> {
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 Tree,
80 progress: &'a Progress<ProgressBar>,
81 ) -> Self {
82 Self {
83 rockspec,
84 rock_bytes,
85 source,
86 config,
87 tree,
88 progress,
89 constraint: LockConstraint::default(),
90 behaviour: BuildBehaviour::default(),
91 pin: PinnedState::default(),
92 opt: OptState::default(),
93 entry_type,
94 }
95 }
96
97 pub(crate) fn pin(self, pin: PinnedState) -> Self {
98 Self { pin, ..self }
99 }
100
101 pub(crate) fn opt(self, opt: OptState) -> Self {
102 Self { opt, ..self }
103 }
104
105 pub(crate) fn constraint(self, constraint: LockConstraint) -> Self {
106 Self { constraint, ..self }
107 }
108
109 pub(crate) fn behaviour(self, behaviour: BuildBehaviour) -> Self {
110 Self { behaviour, ..self }
111 }
112
113 pub(crate) async fn install(self) -> Result<LocalPackage, InstallBinaryRockError> {
114 let rockspec = self.rockspec;
115 self.progress.map(|p| {
116 p.set_message(format!(
117 "Unpacking and installing {}@{}...",
118 rockspec.package(),
119 rockspec.version()
120 ))
121 });
122 for (name, dep) in rockspec.external_dependencies().current_platform() {
123 let _ = ExternalDependencyInfo::probe(name, dep, self.config.external_deps())?;
124 }
125
126 rockspec.validate_lua_version_from_config(self.config)?;
127
128 let hashes = LocalPackageHashes {
129 rockspec: rockspec.hash()?,
130 source: self.rock_bytes.hash()?,
131 };
132 let source_url = match &self.source {
133 RemotePackageSource::LuarocksBinaryRock(url) => {
134 Some(RemotePackageSourceUrl::Url { url: url.clone() })
135 }
136 _ => None,
137 };
138 let mut package = LocalPackage::from(
139 &PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
140 self.constraint,
141 rockspec.binaries(),
142 self.source,
143 source_url,
144 hashes,
145 );
146 package.spec.pinned = self.pin;
147 package.spec.opt = self.opt;
148 match self.tree.lockfile()?.get(&package.id()) {
149 Some(package) if self.behaviour == BuildBehaviour::NoForce => Ok(package.clone()),
150 _ => {
151 let unpack_dir = tempdir()?;
152 let cursor = Cursor::new(self.rock_bytes);
153 let mut zip = zip::ZipArchive::new(cursor)?;
154 zip.extract(&unpack_dir)?;
155 let rock_manifest_file = unpack_dir.path().join("rock_manifest");
161 if !rock_manifest_file.is_file() {
162 return Err(InstallBinaryRockError::RockManifestNotFound);
163 }
164 let rock_manifest_content = tokio::fs::read_to_string(rock_manifest_file).await?;
165 let output_paths = match self.entry_type {
166 tree::EntryType::Entrypoint => self.tree.entrypoint(&package)?,
167 tree::EntryType::DependencyOnly => self.tree.dependency(&package)?,
168 };
169 let rock_manifest = RockManifest::new(&rock_manifest_content)?;
170 install_manifest_entries(
171 &rock_manifest.lib.entries,
172 &unpack_dir.path().join("lib"),
173 &output_paths.lib,
174 )
175 .await?;
176 install_manifest_entries(
177 &rock_manifest.lua.entries,
178 &unpack_dir.path().join("lua"),
179 &output_paths.src,
180 )
181 .await?;
182 install_manifest_entries(
183 &rock_manifest.bin.entries,
184 &unpack_dir.path().join("bin"),
185 &output_paths.bin,
186 )
187 .await?;
188 install_manifest_entries(
189 &rock_manifest.doc.entries,
190 &unpack_dir.path().join("doc"),
191 &output_paths.doc,
192 )
193 .await?;
194 install_manifest_entries(
195 &rock_manifest.root.entries,
196 unpack_dir.path(),
197 &output_paths.etc,
198 )
199 .await?;
200 let rockspec_path = output_paths.etc.join(format!(
202 "{}-{}.rockspec",
203 package.name(),
204 package.version()
205 ));
206 if rockspec_path.is_file() {
207 tokio::fs::copy(&rockspec_path, output_paths.rockspec_path()).await?;
208 tokio::fs::remove_file(&rockspec_path).await?;
209 }
210 Ok(package)
211 }
212 }
213 }
214}
215
216async fn install_manifest_entries<T>(
217 entry: &HashMap<PathBuf, T>,
218 src: &Path,
219 dest: &Path,
220) -> Result<(), InstallBinaryRockError> {
221 for relative_src_path in entry.keys() {
222 let target = dest.join(relative_src_path);
223 let src_path = src.join(relative_src_path);
224 if src_path.is_dir() {
225 recursive_copy_dir(&src_path, &target).await?;
226 } else if src_path.is_file() {
227 if let Some(target_parent_dir) = target.parent() {
228 tokio::fs::create_dir_all(target_parent_dir).await?;
229 }
230 tokio::fs::copy(src.join(relative_src_path), target).await?;
231 } else {
232 let metadata = tokio::fs::metadata(&src_path).await?;
233 return Err(InstallBinaryRockError::NotAFileOrDirectory(
234 src_path.to_string_lossy().to_string(),
235 metadata,
236 ));
237 }
238 }
239 Ok(())
240}
241
242#[cfg(test)]
243mod test {
244
245 use io::Read;
246
247 use crate::{
248 config::ConfigBuilder,
249 operations::{unpack_rockspec, DownloadedPackedRockBytes},
250 progress::MultiProgress,
251 };
252
253 use super::*;
254
255 #[tokio::test]
256 async fn install_binary_rock() {
257 let content = std::fs::read("resources/test/sample-project-0.1.0-1.all.rock").unwrap();
258 let rock_bytes = Bytes::copy_from_slice(&content);
259 let packed_rock_file_name = "sample-project-0.1.0-1.all.rock".to_string();
260 let cursor = Cursor::new(rock_bytes.clone());
261 let mut zip = zip::ZipArchive::new(cursor).unwrap();
262 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
263 let mut manifest_file = zip.by_index(manifest_index).unwrap();
264 let mut content = String::new();
265 manifest_file.read_to_string(&mut content).unwrap();
266 let rock = DownloadedPackedRockBytes {
267 name: "sample-project".into(),
268 version: "0.1.0-1".parse().unwrap(),
269 bytes: rock_bytes,
270 file_name: packed_rock_file_name.clone(),
271 url: "https://test.org".parse().unwrap(),
272 };
273 let rockspec = unpack_rockspec(&rock).await.unwrap();
274 let install_root = assert_fs::TempDir::new().unwrap();
275 let config = ConfigBuilder::new()
276 .unwrap()
277 .user_tree(Some(install_root.to_path_buf()))
278 .build()
279 .unwrap();
280 let progress = MultiProgress::new(&config);
281 let bar = progress.map(MultiProgress::new_bar);
282 let tree = config
283 .user_tree(config.lua_version().unwrap().clone())
284 .unwrap();
285 let local_package = BinaryRockInstall::new(
286 &rockspec,
287 RemotePackageSource::Test,
288 rock.bytes,
289 tree::EntryType::Entrypoint,
290 &config,
291 &tree,
292 &bar,
293 )
294 .install()
295 .await
296 .unwrap();
297 let rock_layout = tree.entrypoint_layout(&local_package);
298 let foo_bar_module = rock_layout.src.join("foo").join("bar.lua");
299 assert!(foo_bar_module.is_file());
300 }
301
302 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
313 #[tokio::test]
314 async fn install_binary_rock_roundtrip() {
315 use crate::operations::{Pack, Uninstall};
316
317 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
318 println!("Skipping impure test");
319 return;
320 }
321 let content = std::fs::read("resources/test/toml-edit-0.6.0-1.linux-x86_64.rock").unwrap();
322 let rock_bytes = Bytes::copy_from_slice(&content);
323 let packed_rock_file_name = "toml-edit-0.6.0-1.linux-x86_64.rock".to_string();
324 let cursor = Cursor::new(rock_bytes.clone());
325 let mut zip = zip::ZipArchive::new(cursor).unwrap();
326 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
327 let mut manifest_file = zip.by_index(manifest_index).unwrap();
328 let mut content = String::new();
329 manifest_file.read_to_string(&mut content).unwrap();
330 let orig_manifest = RockManifest::new(&content).unwrap();
331 let rock = DownloadedPackedRockBytes {
332 name: "toml-edit".into(),
333 version: "0.6.0-1".parse().unwrap(),
334 bytes: rock_bytes,
335 file_name: packed_rock_file_name.clone(),
336 url: "https://test.org".parse().unwrap(),
337 };
338 let rockspec = unpack_rockspec(&rock).await.unwrap();
339 let install_root = assert_fs::TempDir::new().unwrap();
340 let config = ConfigBuilder::new()
341 .unwrap()
342 .user_tree(Some(install_root.to_path_buf()))
343 .build()
344 .unwrap();
345 let progress = MultiProgress::new(&config);
346 let bar = progress.map(MultiProgress::new_bar);
347 let tree = config
348 .user_tree(config.lua_version().unwrap().clone())
349 .unwrap();
350 let local_package = BinaryRockInstall::new(
351 &rockspec,
352 RemotePackageSource::Test,
353 rock.bytes,
354 tree::EntryType::Entrypoint,
355 &config,
356 &tree,
357 &bar,
358 )
359 .install()
360 .await
361 .unwrap();
362 let rock_layout = tree.entrypoint_layout(&local_package);
363
364 assert!(rock_layout.lib.join("toml_edit.so").is_file());
365
366 let orig_install_tree_integrity = rock_layout.rock_path.hash().unwrap();
367
368 let pack_dest_dir = assert_fs::TempDir::new().unwrap();
369 let packed_rock = Pack::new(
370 pack_dest_dir.to_path_buf(),
371 tree.clone(),
372 local_package.clone(),
373 )
374 .pack()
375 .await
376 .unwrap();
377 assert_eq!(
378 packed_rock
379 .file_name()
380 .unwrap()
381 .to_string_lossy()
382 .to_string(),
383 packed_rock_file_name.clone()
384 );
385
386 Uninstall::new()
388 .config(&config)
389 .package(local_package.id())
390 .remove()
391 .await
392 .unwrap();
393 let content = std::fs::read(&packed_rock).unwrap();
394 let rock_bytes = Bytes::copy_from_slice(&content);
395 let cursor = Cursor::new(rock_bytes.clone());
396 let mut zip = zip::ZipArchive::new(cursor).unwrap();
397 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
398 let mut manifest_file = zip.by_index(manifest_index).unwrap();
399 let mut content = String::new();
400 manifest_file.read_to_string(&mut content).unwrap();
401 let packed_manifest = RockManifest::new(&content).unwrap();
402 assert_eq!(packed_manifest, orig_manifest);
403 let rock = DownloadedPackedRockBytes {
404 name: "toml-edit".into(),
405 version: "0.6.0-1".parse().unwrap(),
406 bytes: rock_bytes,
407 file_name: packed_rock_file_name.clone(),
408 url: "https://test.org".parse().unwrap(),
409 };
410 let rockspec = unpack_rockspec(&rock).await.unwrap();
411 let local_package = BinaryRockInstall::new(
412 &rockspec,
413 RemotePackageSource::Test,
414 rock.bytes,
415 tree::EntryType::Entrypoint,
416 &config,
417 &tree,
418 &bar,
419 )
420 .install()
421 .await
422 .unwrap();
423 let rock_layout = tree.entrypoint_layout(&local_package);
424 assert!(rock_layout.rockspec_path().is_file());
425 let new_install_tree_integrity = rock_layout.rock_path.hash().unwrap();
426 assert_eq!(orig_install_tree_integrity, new_install_tree_integrity);
427 }
428}