lux_lib/operations/
pack.rs

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/// A binary rock packer
35#[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    #[error("expected a `package.rockspec` in the package root.")]
62    MissingRockspec,
63}
64
65async fn do_pack(args: Pack) -> Result<PathBuf, PackError> {
66    let package = args.package;
67    let tree = args.tree;
68    let layout = tree.entrypoint_layout(&package);
69    let suffix = if is_binary_rock(&layout) {
70        format!("{}.rock", luarocks::current_platform_luarocks_identifier())
71    } else {
72        "all.rock".into()
73    };
74    let file_name = format!("{}-{}.{}", package.name(), package.version(), suffix);
75    let output_path = args.dest_dir.join(file_name);
76    let file = File::create(&output_path)?;
77    let mut zip = ZipWriter::new(file);
78
79    let lua_entries = add_rock_entries(&mut zip, &layout.src, "lua".into())?;
80    let lib_entries = add_rock_entries(&mut zip, &layout.lib, "lib".into())?;
81    let doc_entries = add_rock_entries(&mut zip, &layout.doc, "doc".into())?;
82    // We copy entries from `etc` to the root directory, as luarocks doesn't have an etc directory.
83    let temp_dir = TempDir::new("lux-pack-temp-root").unwrap().into_path();
84    utils::recursive_copy_dir(&layout.etc, &temp_dir).await?;
85    // prevent duplicate doc entries
86    let doc = temp_dir.join("doc");
87    if doc.is_dir() {
88        std::fs::remove_dir_all(&doc)?;
89    }
90    // luarocks expects a <package>-<version>.rockspec,
91    // so we copy it the package.rockspec to our temporary root directory and rename it
92    if !layout.rockspec_path().is_file() {
93        return Err(PackError::MissingRockspec);
94    }
95    let packed_rockspec_name = format!("{}-{}.rockspec", &package.name(), &package.version());
96    let renamed_rockspec_entry = temp_dir.join(packed_rockspec_name);
97    std::fs::copy(layout.rockspec_path(), &renamed_rockspec_entry)?;
98    let root_entries = add_root_rock_entries(&mut zip, &temp_dir, "".into())?;
99    let mut bin_entries = HashMap::new();
100    for relative_binary_path in package.spec.binaries() {
101        let binary_path = tree.bin().join(
102            relative_binary_path
103                .clean()
104                .file_name()
105                .expect("malformed binary path"),
106        );
107        if binary_path.is_file() {
108            let (path, digest) =
109                add_rock_entry(&mut zip, binary_path, &layout.bin, &PathBuf::default())?;
110            bin_entries.insert(path, digest);
111        }
112    }
113    let rock_manifest = RockManifest {
114        lua: RockManifestLua {
115            entries: lua_entries,
116        },
117        lib: RockManifestLib {
118            entries: lib_entries,
119        },
120        doc: RockManifestDoc {
121            entries: doc_entries,
122        },
123        root: RockManifestRoot {
124            entries: root_entries,
125        },
126        bin: RockManifestBin {
127            entries: bin_entries,
128        },
129    };
130    let manifest_str = rock_manifest.to_lua_string();
131    let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
132    zip.start_file("rock_manifest", options)?;
133    zip.write_all(manifest_str.as_bytes())?;
134    Ok(output_path)
135}
136
137fn is_binary_rock(layout: &RockLayout) -> bool {
138    if !&layout.lib.is_dir() {
139        return false;
140    }
141    WalkDir::new(&layout.lib).into_iter().any(|entry| {
142        entry.is_ok_and(|entry| {
143            let file = entry.into_path();
144            file.is_file()
145                && file
146                    .extension()
147                    .is_some_and(|ext| ext.to_string_lossy() == c_dylib_extension())
148        })
149    })
150}
151
152fn add_root_rock_entries(
153    zip: &mut ZipWriter<File>,
154    source_dir: &PathBuf,
155    zip_dir: PathBuf,
156) -> Result<HashMap<PathBuf, String>, PackError> {
157    let mut result = HashMap::new();
158    if source_dir.is_dir() {
159        for file in WalkDir::new(source_dir).into_iter().filter_map_ok(|entry| {
160            let file = entry.into_path();
161            if file.is_file() {
162                Some(file)
163            } else {
164                None
165            }
166        }) {
167            let file = file?;
168            let (relative_path, digest) = add_rock_entry(zip, file, source_dir, &zip_dir)?;
169            result.insert(relative_path, digest);
170        }
171    }
172    Ok(result)
173}
174
175fn add_rock_entries(
176    zip: &mut ZipWriter<File>,
177    source_dir: &PathBuf,
178    zip_dir: PathBuf,
179) -> Result<HashMap<PathBuf, DirOrFileEntry>, PackError> {
180    let mut result = HashMap::new();
181    if source_dir.is_dir() {
182        for file in WalkDir::new(source_dir).into_iter().filter_map_ok(|entry| {
183            let file = entry.into_path();
184            if file.is_file() {
185                Some(file)
186            } else {
187                None
188            }
189        }) {
190            let file = file?;
191            let (relative_path, digest) = add_rock_entry(zip, file, source_dir, &zip_dir)?;
192            add_dir_or_file_entry(&mut result, &relative_path, digest);
193        }
194    }
195    Ok(result)
196}
197
198fn add_rock_entry(
199    zip: &mut ZipWriter<File>,
200    file: PathBuf,
201    source_dir: &PathBuf,
202    zip_dir: &Path,
203) -> Result<(PathBuf, String), PackError> {
204    let relative_path: PathBuf = pathdiff::diff_paths(source_dir.join(file.clone()), source_dir)
205        .expect("failed get relative path!");
206    let mut f = File::open(file)?;
207    let mut buffer = Vec::new();
208    f.read_to_end(&mut buffer)?;
209    let digest = md5::compute(&buffer);
210
211    #[cfg(target_family = "unix")]
212    let options = SimpleFileOptions::default()
213        .compression_method(zip::CompressionMethod::Stored)
214        .unix_permissions(f.metadata()?.permissions().mode());
215    #[cfg(target_family = "windows")]
216    let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
217
218    zip.start_file(zip_dir.join(&relative_path).to_string_lossy(), options)?;
219    zip.write_all(&buffer)?;
220    Ok((relative_path, format!("{:x}", digest)))
221}
222
223fn add_dir_or_file_entry(
224    dir_map: &mut HashMap<PathBuf, DirOrFileEntry>,
225    relative_path: &Path,
226    digest: String,
227) {
228    let mut components = relative_path
229        .components()
230        .filter_map(|component| match component {
231            std::path::Component::Normal(path) => Some(PathBuf::from(path)),
232            _ => None,
233        })
234        .collect::<VecDeque<_>>();
235    match &components.len() {
236        n if *n > 1 => {
237            let mut entries = HashMap::new();
238            let first_dir = components.pop_front().unwrap();
239            let remainder = components.iter().collect::<PathBuf>();
240            add_dir_or_file_entry(&mut entries, &remainder, digest);
241            dir_map.insert(first_dir, DirOrFileEntry::DirEntry(entries));
242        }
243        _ => {
244            dir_map.insert(
245                relative_path.to_path_buf(),
246                DirOrFileEntry::FileEntry(digest),
247            );
248        }
249    }
250}