1use crate::build::utils;
2use crate::build::utils::c_dylib_extension;
3use crate::fs;
4use crate::lockfile::LocalPackage;
5use crate::luarocks;
6use crate::luarocks::rock_manifest::DirOrFileEntry;
7use crate::luarocks::rock_manifest::RockManifest;
8use crate::luarocks::rock_manifest::RockManifestBin;
9use crate::luarocks::rock_manifest::RockManifestConf;
10use crate::luarocks::rock_manifest::RockManifestDoc;
11use crate::luarocks::rock_manifest::RockManifestLib;
12use crate::luarocks::rock_manifest::RockManifestLua;
13use crate::luarocks::rock_manifest::RockManifestRoot;
14use crate::tree::InstallTree;
15use crate::tree::RockLayout;
16use crate::tree::Tree;
17use bon::Builder;
18use clean_path::Clean;
19use itertools::Itertools;
20use miette::Diagnostic;
21use std::collections::HashMap;
22use std::collections::VecDeque;
23use std::fs::File;
24use std::io;
25use std::io::Read;
26use std::io::Write;
27use std::path::Path;
28use std::path::PathBuf;
29use thiserror::Error;
30use walkdir::WalkDir;
31use zip::write::SimpleFileOptions;
32use zip::ZipWriter;
33
34#[cfg(unix)]
35use std::os::unix::fs::PermissionsExt;
36
37#[derive(Builder)]
39#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
40pub struct Pack {
41 #[builder(start_fn)]
42 dest_dir: PathBuf,
43 #[builder(start_fn)]
44 tree: Tree,
45 #[builder(start_fn)]
46 package: LocalPackage,
47}
48
49impl<State> PackBuilder<State>
50where
51 State: pack_builder::State + pack_builder::IsComplete,
52{
53 pub async fn pack(self) -> Result<PathBuf, PackError> {
54 do_pack(self._build()).await
55 }
56}
57
58#[derive(Error, Debug, Diagnostic)]
59#[error("failed to pack rock: {0}")]
60pub enum PackError {
61 Zip(#[from] zip::result::ZipError),
62 Io(#[from] io::Error),
63 #[error(transparent)]
64 #[diagnostic(transparent)]
65 Fs(#[from] fs::FsError),
66 Walkdir(#[from] walkdir::Error),
67 #[error("expected a `package.rockspec` in the package root.")]
68 MissingRockspec,
69}
70
71async fn do_pack(args: Pack) -> Result<PathBuf, PackError> {
72 let package = args.package;
73 let tree = args.tree;
74 let layout = tree.entrypoint_layout(&package);
75 let suffix = if is_binary_rock(&layout) {
76 format!("{}.rock", luarocks::current_platform_luarocks_identifier())
77 } else {
78 "all.rock".into()
79 };
80 let file_name = format!("{}-{}.{}", package.name(), package.version(), suffix);
81 let temp_file_name = format!("{}-{}.{}.part", package.name(), package.version(), suffix);
82 let temp_output_path = args.dest_dir.join(temp_file_name);
83 let output_path = args.dest_dir.join(file_name);
84 let file = fs::sync::create(&temp_output_path)?;
85 let mut zip = ZipWriter::new(file);
86
87 let lua_entries = add_rock_entries(&mut zip, &layout.src, "lua".into())?;
88 let lib_entries = add_rock_entries(&mut zip, &layout.lib, "lib".into())?;
89 let doc_entries = add_rock_entries(&mut zip, &layout.doc, "doc".into())?;
90 let conf_entries = add_rock_entries(&mut zip, &layout.conf, "conf".into())?;
91 let temp_root_dir = fs::tempfile::tempdir()?;
93 utils::recursive_copy_dir(&layout.etc, temp_root_dir.path()).await?;
94 let doc = temp_root_dir.path().join("doc");
96 if doc.is_dir() {
97 fs::tokio::remove_dir_all(&doc).await?;
98 }
99 let conf = temp_root_dir.path().join("conf");
100 if conf.is_dir() {
101 fs::tokio::remove_dir_all(&conf).await?;
102 }
103 if !layout.rockspec_path().is_file() {
106 return Err(PackError::MissingRockspec);
107 }
108 let packed_rockspec_name = format!("{}-{}.rockspec", &package.name(), &package.version());
109 let renamed_rockspec_entry = temp_root_dir.path().join(packed_rockspec_name);
110 fs::tokio::copy(layout.rockspec_path(), &renamed_rockspec_entry).await?;
111 let root_entries = add_rock_entries(&mut zip, temp_root_dir.path(), "".into())?;
112 let mut bin_entries = HashMap::new();
113 for relative_binary_path in package.spec.binaries() {
114 if let Some(binary_name) = relative_binary_path.clean().file_name() {
115 let binary_path = tree.bin().join(binary_name);
116 if binary_path.is_file() {
117 let (path, digest) =
118 add_rock_entry(&mut zip, binary_path, &layout.bin, &PathBuf::default())?;
119 bin_entries.insert(path, digest);
120 }
121 }
122 }
123 let rock_manifest = RockManifest {
124 lua: RockManifestLua {
125 entries: lua_entries,
126 },
127 lib: RockManifestLib {
128 entries: lib_entries,
129 },
130 doc: RockManifestDoc {
131 entries: doc_entries,
132 },
133 conf: RockManifestConf {
134 entries: conf_entries,
135 },
136 root: RockManifestRoot {
137 entries: root_entries,
138 },
139 bin: RockManifestBin {
140 entries: bin_entries,
141 },
142 };
143 let manifest_str = rock_manifest.to_lua_string();
144 let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
145 zip.start_file("rock_manifest", options)?;
146 zip.write_all(manifest_str.as_bytes())?;
147 fs::tokio::rename(&temp_output_path, &output_path).await?;
148 Ok(output_path)
149}
150
151fn is_binary_rock(layout: &RockLayout) -> bool {
152 if !&layout.lib.is_dir() {
153 return false;
154 }
155 WalkDir::new(&layout.lib).into_iter().any(|entry| {
156 entry.is_ok_and(|entry| {
157 let file = entry.into_path();
158 file.is_file()
159 && file
160 .extension()
161 .is_some_and(|ext| ext.to_string_lossy() == c_dylib_extension())
162 })
163 })
164}
165
166fn add_rock_entries(
167 zip: &mut ZipWriter<File>,
168 source_dir: &Path,
169 zip_dir: PathBuf,
170) -> Result<HashMap<PathBuf, DirOrFileEntry>, PackError> {
171 let mut result = HashMap::new();
172 if source_dir.is_dir() {
173 for file in WalkDir::new(source_dir).into_iter().filter_map_ok(|entry| {
174 let file = entry.into_path();
175 if file.is_file() {
176 Some(file)
177 } else {
178 None
179 }
180 }) {
181 let file = file?;
182 let (relative_path, digest) = add_rock_entry(zip, file, source_dir, &zip_dir)?;
183 add_dir_or_file_entry(&mut result, &relative_path, digest);
184 }
185 }
186 Ok(result)
187}
188
189fn add_rock_entry(
190 zip: &mut ZipWriter<File>,
191 file: PathBuf,
192 source_dir: &Path,
193 zip_dir: &Path,
194) -> Result<(PathBuf, String), PackError> {
195 let relative_path: PathBuf = unsafe {
196 pathdiff::diff_paths(source_dir.join(file.clone()), source_dir).unwrap_unchecked()
197 };
198 let mut f = fs::sync::open(file)?;
199 let mut buffer = Vec::new();
200 f.read_to_end(&mut buffer)?;
201 let digest = md5::compute(&buffer);
202
203 #[cfg(target_family = "unix")]
204 let options = SimpleFileOptions::default()
205 .compression_method(zip::CompressionMethod::Stored)
206 .unix_permissions(f.metadata()?.permissions().mode());
207 #[cfg(target_family = "windows")]
208 let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
209
210 zip.start_file(zip_dir.join(&relative_path).to_string_lossy(), options)?;
211 zip.write_all(&buffer)?;
212 Ok((relative_path, format!("{digest:x}")))
213}
214
215fn add_dir_or_file_entry(
216 dir_map: &mut HashMap<PathBuf, DirOrFileEntry>,
217 relative_path: &Path,
218 digest: String,
219) {
220 let mut components = relative_path
221 .components()
222 .filter_map(|component| match component {
223 std::path::Component::Normal(path) => Some(PathBuf::from(path)),
224 _ => None,
225 })
226 .collect::<VecDeque<_>>();
227 match &components.len() {
228 n if *n > 1 => {
229 if let Some(first_dir) = components.pop_front() {
230 let mut entries = HashMap::new();
231 let remainder = components.iter().collect::<PathBuf>();
232 add_dir_or_file_entry(&mut entries, &remainder, digest);
233 dir_map.insert(first_dir, DirOrFileEntry::DirEntry(entries));
234 }
235 }
236 _ => {
237 dir_map.insert(
238 relative_path.to_path_buf(),
239 DirOrFileEntry::FileEntry(digest),
240 );
241 }
242 }
243}