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