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