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