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