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 remote_package_source::RemotePackageSource,
22 rockspec::Rockspec,
23 tree::{self, InstallTree, TreeError},
24};
25use crate::{lockfile::RemotePackageSourceUrl, rockspec::LuaVersionCompatibility};
26use bytes::Bytes;
27use miette::Diagnostic;
28use tempfile::tempdir;
29use thiserror::Error;
30
31use super::rock_manifest::RockManifestError;
32
33#[derive(Error, Debug, Diagnostic)]
34#[non_exhaustive]
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}
78
79impl<'a, T> BinaryRockInstall<'a, T>
80where
81 T: InstallTree,
82{
83 pub(crate) fn new(
84 rockspec: &'a RemoteLuaRockspec,
85 source: RemotePackageSource,
86 rock_bytes: Bytes,
87 entry_type: tree::EntryType,
88 config: &'a Config,
89 tree: &'a T,
90 ) -> Self {
91 Self {
92 rockspec,
93 rock_bytes,
94 source,
95 config,
96 tree,
97 constraint: LockConstraint::default(),
98 behaviour: BuildBehaviour::default(),
99 pin: PinnedState::default(),
100 opt: OptState::default(),
101 entry_type,
102 }
103 }
104
105 pub(crate) fn pin(self, pin: PinnedState) -> Self {
106 Self { pin, ..self }
107 }
108
109 pub(crate) fn opt(self, opt: OptState) -> Self {
110 Self { opt, ..self }
111 }
112
113 pub(crate) fn constraint(self, constraint: LockConstraint) -> Self {
114 Self { constraint, ..self }
115 }
116
117 pub(crate) fn behaviour(self, behaviour: BuildBehaviour) -> Self {
118 Self { behaviour, ..self }
119 }
120
121 #[tracing::instrument(name = "Installing binary rock", skip_all)]
122 pub(crate) async fn install(self) -> Result<LocalPackage, InstallBinaryRockError> {
123 let rockspec = self.rockspec;
124 for (name, dep) in rockspec.external_dependencies().current_platform() {
125 let _ = ExternalDependencyInfo::probe(name, dep, self.config.external_deps())?;
126 }
127
128 rockspec.validate_lua_version_from_config(self.config)?;
129
130 let hashes = LocalPackageHashes {
131 rockspec: rockspec.hash()?,
132 source: self.rock_bytes.hash()?,
133 };
134 let source_url = match &self.source {
135 RemotePackageSource::LuarocksBinaryRock(url) => {
136 Some(RemotePackageSourceUrl::Url { url: url.clone() })
137 }
138 _ => None,
139 };
140 let mut package = LocalPackage::from(
141 &PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
142 self.constraint,
143 rockspec.binaries(),
144 self.source,
145 source_url,
146 hashes,
147 );
148 package.spec.pinned = self.pin;
149 package.spec.opt = self.opt;
150 match self.tree.lockfile()?.get(&package.id()) {
151 Some(package) if self.behaviour == BuildBehaviour::NoForce => Ok(package.clone()),
152 _ => {
153 let unpack_dir = tempdir()?;
154 let cursor = Cursor::new(self.rock_bytes);
155 let mut zip = zip::ZipArchive::new(cursor)?;
156 zip.extract(&unpack_dir)?;
157 let rock_manifest_file = unpack_dir.path().join("rock_manifest");
163 if !rock_manifest_file.is_file() {
164 return Err(InstallBinaryRockError::RockManifestNotFound);
165 }
166 let rock_manifest_content = tokio::fs::read_to_string(rock_manifest_file).await?;
167 let output_paths = match self.entry_type {
168 tree::EntryType::Entrypoint => self.tree.entrypoint(&package)?,
169 tree::EntryType::DependencyOnly => self.tree.dependency(&package)?,
170 };
171 let rock_manifest = RockManifest::new(&rock_manifest_content)?;
172 install_manifest_entries(
173 &rock_manifest.lib.entries,
174 &unpack_dir.path().join("lib"),
175 &output_paths.lib,
176 )
177 .await?;
178 install_manifest_entries(
179 &rock_manifest.lua.entries,
180 &unpack_dir.path().join("lua"),
181 &output_paths.src,
182 )
183 .await?;
184 install_manifest_entries(
185 &rock_manifest.bin.entries,
186 &unpack_dir.path().join("bin"),
187 &output_paths.bin,
188 )
189 .await?;
190 install_manifest_entries(
191 &rock_manifest.doc.entries,
192 &unpack_dir.path().join("doc"),
193 &output_paths.doc,
194 )
195 .await?;
196 install_manifest_entries(
197 &rock_manifest.root.entries,
198 unpack_dir.path(),
199 &output_paths.etc,
200 )
201 .await?;
202 let rockspec_path = output_paths.etc.join(format!(
204 "{}-{}.rockspec",
205 package.name(),
206 package.version()
207 ));
208 if rockspec_path.is_file() {
209 tokio::fs::copy(&rockspec_path, output_paths.rockspec_path()).await?;
210 tokio::fs::remove_file(&rockspec_path).await?;
211 }
212 Ok(package)
213 }
214 }
215 }
216}
217
218#[tracing::instrument(level = "trace", skip(entry))]
219async fn install_manifest_entries<T>(
220 entry: &HashMap<PathBuf, T>,
221 src: &Path,
222 dest: &Path,
223) -> Result<(), InstallBinaryRockError> {
224 for relative_src_path in entry.keys() {
225 let target = dest.join(relative_src_path);
226 let src_path = src.join(relative_src_path);
227 if src_path.is_dir() {
228 recursive_copy_dir(&src_path, &target).await?;
229 } else if src_path.is_file() {
230 if let Some(target_parent_dir) = target.parent() {
231 tokio::fs::create_dir_all(target_parent_dir).await?;
232 }
233 tokio::fs::copy(src.join(relative_src_path), target).await?;
234 } else {
235 let metadata = tokio::fs::metadata(&src_path).await?;
236 return Err(InstallBinaryRockError::NotAFileOrDirectory(
237 src_path.to_string_lossy().to_string(),
238 metadata,
239 ));
240 }
241 }
242 Ok(())
243}
244
245#[cfg(test)]
246mod tests {
247
248 use io::Read;
249
250 use crate::{
251 config::ConfigBuilder,
252 operations::{unpack_rockspec, DownloadedPackedRockBytes},
253 };
254
255 use super::*;
256
257 #[tokio::test]
258 async fn install_binary_rock() {
259 let rock = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
260 .join("resources/test/sample-project-0.1.0-1.all.rock");
261 let content = std::fs::read(rock).unwrap();
262 let rock_bytes = Bytes::copy_from_slice(&content);
263 let packed_rock_file_name = "sample-project-0.1.0-1.all.rock".to_string();
264 let cursor = Cursor::new(rock_bytes.clone());
265 let mut zip = zip::ZipArchive::new(cursor).unwrap();
266 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
267 let mut manifest_file = zip.by_index(manifest_index).unwrap();
268 let mut content = String::new();
269 manifest_file.read_to_string(&mut content).unwrap();
270 let rock = DownloadedPackedRockBytes {
271 name: "sample-project".into(),
272 version: "0.1.0-1".parse().unwrap(),
273 bytes: rock_bytes,
274 file_name: packed_rock_file_name.clone(),
275 url: "https://test.org".parse().unwrap(),
276 };
277 let rockspec = unpack_rockspec(&rock).await.unwrap();
278 let install_root = assert_fs::TempDir::new().unwrap();
279 let config = ConfigBuilder::new()
280 .unwrap()
281 .user_tree(Some(install_root.to_path_buf()))
282 .build()
283 .unwrap();
284 let tree = config
285 .user_tree(config.lua_version().unwrap().clone())
286 .unwrap();
287 let local_package = BinaryRockInstall::new(
288 &rockspec,
289 RemotePackageSource::Test,
290 rock.bytes,
291 tree::EntryType::Entrypoint,
292 &config,
293 &tree,
294 )
295 .install()
296 .await
297 .unwrap();
298 let rock_layout = tree.entrypoint_layout(&local_package);
299 let foo_bar_module = rock_layout.src.join("foo").join("bar.lua");
300 assert!(foo_bar_module.is_file());
301 }
302
303 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
314 #[tokio::test]
315 async fn install_binary_rock_roundtrip() {
316 use crate::operations::{Pack, Uninstall};
317
318 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
319 println!("Skipping impure test");
320 return;
321 }
322 let rock = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
323 .join("resources/test/toml-edit-0.6.0-1.linux-x86_64.rock");
324 let content = std::fs::read(rock).unwrap();
325 let rock_bytes = Bytes::copy_from_slice(&content);
326 let packed_rock_file_name = "toml-edit-0.6.0-1.linux-x86_64.rock".to_string();
327 let cursor = Cursor::new(rock_bytes.clone());
328 let mut zip = zip::ZipArchive::new(cursor).unwrap();
329 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
330 let mut manifest_file = zip.by_index(manifest_index).unwrap();
331 let mut content = String::new();
332 manifest_file.read_to_string(&mut content).unwrap();
333 let orig_manifest = RockManifest::new(&content).unwrap();
334 let rock = DownloadedPackedRockBytes {
335 name: "toml-edit".into(),
336 version: "0.6.0-1".parse().unwrap(),
337 bytes: rock_bytes,
338 file_name: packed_rock_file_name.clone(),
339 url: "https://test.org".parse().unwrap(),
340 };
341 let rockspec = unpack_rockspec(&rock).await.unwrap();
342 let install_root = assert_fs::TempDir::new().unwrap();
343 let config = ConfigBuilder::new()
344 .unwrap()
345 .user_tree(Some(install_root.to_path_buf()))
346 .build()
347 .unwrap();
348 let tree = config
349 .user_tree(config.lua_version().unwrap().clone())
350 .unwrap();
351 let local_package = BinaryRockInstall::new(
352 &rockspec,
353 RemotePackageSource::Test,
354 rock.bytes,
355 tree::EntryType::Entrypoint,
356 &config,
357 &tree,
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 )
419 .install()
420 .await
421 .unwrap();
422 let rock_layout = tree.entrypoint_layout(&local_package);
423 assert!(rock_layout.rockspec_path().is_file());
424 let new_install_tree_integrity = rock_layout.rock_path.hash().unwrap();
425 assert_eq!(orig_install_tree_integrity, new_install_tree_integrity);
426 }
427}