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