1use std::io::{self, Cursor};
2
3use crate::{
4 build::{
5 external_dependency::{self, ExternalDependencyInfo},
6 BuildBehaviour,
7 },
8 config::Config,
9 hash::HasIntegrity,
10 lockfile::{self, LockedPackage, LockedPackageHashes, LockedPackageSpec},
11 lua_rockspec::{LuaVersionError, RemoteLuaRockspec},
12 remote_package_source::RemotePackageSource,
13 rockspec::Rockspec,
14 tree::{self, InstallTree},
15};
16use crate::{fs, lockfile::RemotePackageSourceUrl, rockspec::LuaVersionCompatibility};
17use bytes::Bytes;
18use miette::Diagnostic;
19use thiserror::Error;
20
21use super::{install_rock_manifest, InstallRockManifestError, RockManifestSources};
22
23#[derive(Error, Debug, Diagnostic)]
24#[non_exhaustive]
25pub enum Error {
26 #[error("IO operation failed")]
27 Io(#[from] io::Error),
28 #[error(transparent)]
29 #[diagnostic(transparent)]
30 Fs(#[from] fs::Error),
31 #[error(transparent)]
32 #[diagnostic(transparent)]
33 Lockfile(#[from] lockfile::Error),
34 #[error(transparent)]
35 #[diagnostic(transparent)]
36 Tree(#[from] tree::Error),
37 #[error(transparent)]
38 #[diagnostic(transparent)]
39 ExternalDependency(#[from] external_dependency::Error),
40 #[error(transparent)]
41 #[diagnostic(transparent)]
42 LuaVersionError(#[from] LuaVersionError),
43 #[error("failed to unpack packed rock")]
44 Zip(#[from] zip::result::ZipError),
45 #[error(transparent)]
46 #[diagnostic(transparent)]
47 InstallRockManifest(#[from] InstallRockManifestError),
48}
49
50pub(crate) struct BinaryRockInstall<'a, T>
51where
52 T: InstallTree,
53{
54 rockspec: &'a RemoteLuaRockspec,
55 spec: LockedPackageSpec,
56 rock_bytes: Bytes,
57 source: RemotePackageSource,
58 entry_type: tree::EntryType,
59 behaviour: BuildBehaviour,
60 config: &'a Config,
61 tree: &'a T,
62}
63
64impl<'a, T> BinaryRockInstall<'a, T>
65where
66 T: InstallTree,
67{
68 pub(crate) fn new(
69 rockspec: &'a RemoteLuaRockspec,
70 spec: LockedPackageSpec,
71 rock_bytes: Bytes,
72 source: RemotePackageSource,
73 entry_type: tree::EntryType,
74 config: &'a Config,
75 tree: &'a T,
76 ) -> Self {
77 Self {
78 rockspec,
79 spec,
80 rock_bytes,
81 source,
82 config,
83 tree,
84 entry_type,
85 behaviour: BuildBehaviour::Ignore,
86 }
87 }
88
89 pub(crate) fn behaviour(self, behaviour: BuildBehaviour) -> Self {
90 Self { behaviour, ..self }
91 }
92
93 #[tracing::instrument(name = "Installing binary rock", skip_all)]
94 pub(crate) async fn install(self) -> Result<LockedPackage, Error> {
95 let rockspec = self.rockspec;
96 for (name, dep) in rockspec.external_dependencies().current_platform() {
97 let _ = ExternalDependencyInfo::probe(name, dep, self.config.external_deps())?;
98 }
99
100 rockspec.validate_lua_version_from_config(self.config)?;
101
102 let hashes = LockedPackageHashes {
103 rockspec: rockspec.hash().await?,
104 source: self.rock_bytes.hash().await?,
105 };
106 let source_url = match &self.source {
107 RemotePackageSource::LuarocksBinaryRock(url) => {
108 Some(RemotePackageSourceUrl::Url { url: url.clone() })
109 }
110 _ => None,
111 };
112 let package = LockedPackage::new(self.spec, self.source, source_url, hashes);
113 match self.tree.lockfile()?.get(&package.id()) {
114 Some(package)
115 if matches!(
116 self.behaviour,
117 BuildBehaviour::Ignore | BuildBehaviour::Conflict
118 ) =>
119 {
120 Ok(package.clone())
121 }
122 _ => {
123 let unpack_dir = fs::tempfile::tempdir()?;
124 let cursor = Cursor::new(self.rock_bytes);
125 let mut zip = zip::ZipArchive::new(cursor)?;
126 zip.extract(&unpack_dir)?;
127 self.tree.prepare(&package.spec)?;
128 let layout = self.tree.layout_for(&package.spec);
129 let sources = RockManifestSources {
130 root: unpack_dir.path().to_path_buf(),
131 lib: unpack_dir.path().join("lib"),
132 lua: unpack_dir.path().join("lua"),
133 bin: unpack_dir.path().join("bin"),
134 };
135 install_rock_manifest(sources, &layout, &self.tree.bin()).await?;
136 let rockspec_path =
138 layout
139 .etc
140 .join(format!("{}-{}.rockspec", package.name(), package.version()));
141 if rockspec_path.is_file() {
142 fs::tokio::copy(&rockspec_path, layout.rockspec_path()).await?;
143 fs::tokio::remove_file(&rockspec_path).await?;
144 }
145 self.tree.finalize(&package, self.entry_type)?;
146 self.tree
147 .lockfile()?
148 .write_guard()
149 .set_binaries(&package, rockspec.binaries());
150 Ok(package)
151 }
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use std::path::PathBuf;
159
160 use assert_fs::TempDir;
161 use io::{Read, Write};
162 use is_executable::IsExecutable;
163 use zip::write::SimpleFileOptions;
164
165 #[cfg(all(target_os = "linux", target_arch = "x86_64", feature = "impure_tests"))]
166 use crate::luarocks::rock_manifest::RockManifest;
167 use crate::{
168 config::ConfigBuilder,
169 lockfile::{LockConstraint, LockedPackageId, OptState, PinnedState},
170 lua_version::LuaVersion,
171 operations::{unpack_rockspec, DownloadedPackedRockBytes},
172 package::PackageName,
173 };
174
175 use super::*;
176 #[cfg(feature = "impure_tests")]
177 use flaky_test::flaky_test;
178
179 impl RemoteLuaRockspec {
180 fn to_locked_package_spec(&self) -> LockedPackageSpec {
181 LockedPackageSpec::new(
182 self.package(),
183 self.version(),
184 LockConstraint::Unconstrained,
185 Vec::new(),
186 Vec::new(),
187 &PinnedState::default(),
188 &OptState::default(),
189 )
190 }
191 }
192
193 #[tokio::test]
194 async fn install_binary_rock() {
195 let rock = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
196 .join("resources/test/sample-project-0.1.0-1.all.rock");
197 let content = std::fs::read(rock).unwrap();
198 let rock_bytes = Bytes::copy_from_slice(&content);
199 let packed_rock_file_name = "sample-project-0.1.0-1.all.rock".to_string();
200 let cursor = Cursor::new(rock_bytes.clone());
201 let mut zip = zip::ZipArchive::new(cursor).unwrap();
202 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
203 let mut manifest_file = zip.by_index(manifest_index).unwrap();
204 let mut content = String::new();
205 manifest_file.read_to_string(&mut content).unwrap();
206 let rock = DownloadedPackedRockBytes {
207 name: "sample-project".into(),
208 version: "0.1.0-1".parse().unwrap(),
209 bytes: rock_bytes,
210 file_name: packed_rock_file_name.clone(),
211 url: "https://test.org".parse().unwrap(),
212 };
213 let rockspec = unpack_rockspec(&rock).await.unwrap();
214 let install_root = assert_fs::TempDir::new().unwrap();
215 let config = ConfigBuilder::new()
216 .unwrap()
217 .user_tree(Some(install_root.to_path_buf()))
218 .build()
219 .unwrap();
220 let tree = config
221 .user_tree(config.lua_version().unwrap().clone())
222 .unwrap();
223 let local_package = BinaryRockInstall::new(
224 &rockspec,
225 rockspec.to_locked_package_spec(),
226 rock.bytes,
227 RemotePackageSource::Test,
228 tree::EntryType::Entrypoint,
229 &config,
230 &tree,
231 )
232 .install()
233 .await
234 .unwrap();
235 assert_eq!(
236 local_package.hashes().source,
237 "sha256-zDmnjE2b8GxX+6j5vTCWUEBxe0aFXFVhC4JOrnAgQ2g="
238 .parse()
239 .unwrap()
240 );
241 let foo_bar_module = tree
242 .layout_for(&local_package.spec)
243 .src
244 .join("foo")
245 .join("bar.lua");
246 assert!(foo_bar_module.is_file());
247 }
248
249 fn packed_rock_with_bin() -> Vec<u8> {
250 let rockspec = r#"
251rockspec_format = "3.0"
252package = "test-binary-bin"
253version = "1.0-1"
254source = { url = "https://example.com/test-binary-bin.zip" }
255"#;
256 let manifest = r#"rock_manifest = { bin = { mytool = "0" } }"#;
257 let mut buf = Vec::new();
258 {
259 let mut zip = zip::ZipWriter::new(Cursor::new(&mut buf));
260 let options = SimpleFileOptions::default();
261 for (name, content) in [
262 ("test-binary-bin-1.0-1.rockspec", rockspec.as_bytes()),
263 ("bin/mytool", b"#!/bin/sh\necho hi\n".as_slice()),
264 ("rock_manifest", manifest.as_bytes()),
265 ] {
266 zip.start_file(name, options).unwrap();
267 zip.write_all(content).unwrap();
268 }
269 zip.finish().unwrap();
270 }
271 buf
272 }
273
274 #[cfg(unix)]
275 #[tokio::test]
276 async fn install_binary_rock_makes_bin_executable() {
277 let rock = DownloadedPackedRockBytes {
278 name: "test-binary-bin".into(),
279 version: "1.0-1".parse().unwrap(),
280 bytes: Bytes::from(packed_rock_with_bin()),
281 file_name: "test-binary-bin-1.0-1.all.rock".into(),
282 url: "https://test.org".parse().unwrap(),
283 };
284 let rockspec = unpack_rockspec(&rock).await.unwrap();
285 let install_root = TempDir::new().unwrap();
286 let config = ConfigBuilder::new()
287 .unwrap()
288 .user_tree(Some(install_root.to_path_buf()))
289 .build()
290 .unwrap();
291 let tree = config
292 .user_tree(config.lua_version().unwrap().clone())
293 .unwrap();
294 BinaryRockInstall::new(
295 &rockspec,
296 rockspec.to_locked_package_spec(),
297 rock.bytes,
298 RemotePackageSource::Test,
299 tree::EntryType::Entrypoint,
300 &config,
301 &tree,
302 )
303 .install()
304 .await
305 .unwrap();
306 assert!(tree.bin().join("mytool").is_executable());
307 }
308
309 #[tokio::test]
310 async fn install_binary_rock_preserves_dependencies() {
311 let rock = DownloadedPackedRockBytes {
312 name: "test-binary-bin".into(),
313 version: "1.0-1".parse().unwrap(),
314 bytes: Bytes::from(packed_rock_with_bin()),
315 file_name: "test-binary-bin-1.0-1.all.rock".into(),
316 url: "https://test.org".parse().unwrap(),
317 };
318 let rockspec = unpack_rockspec(&rock).await.unwrap();
319 let dependency = LockedPackageId::new(
320 &PackageName::from("transitive-dependency"),
321 &"1.0-1".parse().unwrap(),
322 PinnedState::default(),
323 OptState::default(),
324 LockConstraint::Unconstrained,
325 );
326 let spec = LockedPackageSpec::new(
327 rockspec.package(),
328 rockspec.version(),
329 LockConstraint::Unconstrained,
330 vec![dependency.clone()],
331 Vec::new(),
332 &PinnedState::default(),
333 &OptState::default(),
334 );
335 let install_root = TempDir::new().unwrap();
336 let config = ConfigBuilder::new()
337 .unwrap()
338 .lua_version(Some(LuaVersion::Lua51))
339 .user_tree(Some(install_root.to_path_buf()))
340 .build()
341 .unwrap();
342 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
343
344 let installed = BinaryRockInstall::new(
345 &rockspec,
346 spec,
347 rock.bytes,
348 RemotePackageSource::Test,
349 tree::EntryType::Entrypoint,
350 &config,
351 &tree,
352 )
353 .install()
354 .await
355 .unwrap();
356
357 assert_eq!(installed.dependencies(), vec![&dependency]);
358 }
359
360 #[cfg(all(target_os = "linux", target_arch = "x86_64", feature = "impure_tests"))]
371 #[flaky_test(tokio, times = 5)]
372 async fn install_binary_rock_roundtrip() {
373 use crate::operations::{Pack, Uninstall};
374
375 let rock = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
376 .join("resources/test/toml-edit-0.6.0-1.linux-x86_64.rock");
377 let content = std::fs::read(rock).unwrap();
378 let rock_bytes = Bytes::copy_from_slice(&content);
379 let packed_rock_file_name = "toml-edit-0.6.0-1.linux-x86_64.rock".to_string();
380 let cursor = Cursor::new(rock_bytes.clone());
381 let mut zip = zip::ZipArchive::new(cursor).unwrap();
382 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
383 let mut manifest_file = zip.by_index(manifest_index).unwrap();
384 let mut content = String::new();
385 manifest_file.read_to_string(&mut content).unwrap();
386 let orig_manifest = RockManifest::new(&content).unwrap();
387 let rock = DownloadedPackedRockBytes {
388 name: "toml-edit".into(),
389 version: "0.6.0-1".parse().unwrap(),
390 bytes: rock_bytes,
391 file_name: packed_rock_file_name.clone(),
392 url: "https://test.org".parse().unwrap(),
393 };
394 let rockspec = unpack_rockspec(&rock).await.unwrap();
395 let install_root = assert_fs::TempDir::new().unwrap();
396 let config = ConfigBuilder::new()
397 .unwrap()
398 .user_tree(Some(install_root.to_path_buf()))
399 .build()
400 .unwrap();
401 let tree = config
402 .user_tree(config.lua_version().unwrap().clone())
403 .unwrap();
404 let local_package = BinaryRockInstall::new(
405 &rockspec,
406 rockspec.to_locked_package_spec(),
407 rock.bytes,
408 RemotePackageSource::Test,
409 tree::EntryType::Entrypoint,
410 &config,
411 &tree,
412 )
413 .install()
414 .await
415 .unwrap();
416 let layout = tree.layout_for(local_package.spec());
417 assert!(layout.lib.join("toml_edit.so").is_file());
418
419 let orig_install_tree_integrity = layout.root.hash().await.unwrap();
420
421 let pack_dest_dir = assert_fs::TempDir::new().unwrap();
422 let packed_rock = Pack::new(
423 pack_dest_dir.to_path_buf(),
424 tree.clone(),
425 local_package.clone(),
426 )
427 .pack()
428 .await
429 .unwrap();
430 assert_eq!(
431 packed_rock
432 .file_name()
433 .unwrap()
434 .to_string_lossy()
435 .to_string(),
436 packed_rock_file_name.clone()
437 );
438
439 Uninstall::new()
441 .config(&config)
442 .package(local_package.id())
443 .remove()
444 .await
445 .unwrap();
446 let content = std::fs::read(&packed_rock).unwrap();
447 let rock_bytes = Bytes::copy_from_slice(&content);
448 let cursor = Cursor::new(rock_bytes.clone());
449 let mut zip = zip::ZipArchive::new(cursor).unwrap();
450 let manifest_index = zip.index_for_path("rock_manifest").unwrap();
451 let mut manifest_file = zip.by_index(manifest_index).unwrap();
452 let mut content = String::new();
453 manifest_file.read_to_string(&mut content).unwrap();
454 let packed_manifest = RockManifest::new(&content).unwrap();
455 assert_eq!(packed_manifest, orig_manifest);
456 let rock = DownloadedPackedRockBytes {
457 name: "toml-edit".into(),
458 version: "0.6.0-1".parse().unwrap(),
459 bytes: rock_bytes,
460 file_name: packed_rock_file_name.clone(),
461 url: "https://test.org".parse().unwrap(),
462 };
463 let rockspec = unpack_rockspec(&rock).await.unwrap();
464 let local_package = BinaryRockInstall::new(
465 &rockspec,
466 rockspec.to_locked_package_spec(),
467 rock.bytes,
468 RemotePackageSource::Test,
469 tree::EntryType::Entrypoint,
470 &config,
471 &tree,
472 )
473 .install()
474 .await
475 .unwrap();
476 let layout = tree.layout_for(local_package.spec());
477 assert!(layout.rockspec_path().is_file());
478 let new_install_tree_integrity = layout.root.hash().await.unwrap();
479 assert_eq!(orig_install_tree_integrity, new_install_tree_integrity);
480 }
481}