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