Skip to main content

lux_lib/operations/
pack.rs

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