Skip to main content

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