1use crate::error::{Error, Result};
8use flate2::write::GzEncoder;
9use flate2::Compression;
10use sha2::{Digest, Sha256};
11use std::fs::File;
12use std::io::Write;
13use std::path::{Path, PathBuf};
14use tar::Builder;
15use tracing::{debug, info};
16
17#[derive(Debug)]
19pub struct BottleResult {
20 pub path: PathBuf,
22 pub size: u64,
24 pub sha256: String,
26 pub file_count: usize,
28}
29
30pub fn create_bottle(
43 install_path: &Path,
44 output_path: &Path,
45 name: &str,
46 version: &str,
47) -> Result<BottleResult> {
48 info!(
49 "Creating bottle for {} {} from {:?}",
50 name, version, install_path
51 );
52
53 if !install_path.exists() {
54 return Err(Error::Bottle(format!(
55 "Install path does not exist: {:?}",
56 install_path
57 )));
58 }
59
60 let output_file = File::create(output_path)
62 .map_err(|e| Error::Bottle(format!("Failed to create output file: {}", e)))?;
63
64 let encoder = GzEncoder::new(output_file, Compression::default());
65 let mut builder = Builder::new(encoder);
66
67 let base_path = format!("{}/{}", name, version);
70 let mut file_count = 0;
71
72 file_count += add_directory_to_tar(&mut builder, install_path, &base_path)?;
74
75 let encoder = builder
77 .into_inner()
78 .map_err(|e| Error::Bottle(format!("Failed to finish archive: {}", e)))?;
79
80 encoder
81 .finish()
82 .map_err(|e| Error::Bottle(format!("Failed to finish gzip: {}", e)))?;
83
84 let file_bytes = std::fs::read(output_path)
86 .map_err(|e| Error::Bottle(format!("Failed to read bottle: {}", e)))?;
87
88 let mut hasher = Sha256::new();
89 hasher.update(&file_bytes);
90 let sha256 = format!("{:x}", hasher.finalize());
91
92 let size = file_bytes.len() as u64;
93
94 info!("Created bottle: {} bytes, {} files", size, file_count);
95
96 Ok(BottleResult {
97 path: output_path.to_path_buf(),
98 size,
99 sha256,
100 file_count,
101 })
102}
103
104fn add_directory_to_tar<W: Write>(
106 builder: &mut Builder<W>,
107 dir_path: &Path,
108 archive_base: &str,
109) -> Result<usize> {
110 let mut count = 0;
111
112 for entry in std::fs::read_dir(dir_path)
113 .map_err(|e| Error::Bottle(format!("Failed to read directory: {}", e)))?
114 {
115 let entry =
116 entry.map_err(|e| Error::Bottle(format!("Failed to read directory entry: {}", e)))?;
117 let path = entry.path();
118 let file_name = entry.file_name();
119 let archive_path = format!("{}/{}", archive_base, file_name.to_string_lossy());
120
121 if path.is_dir() {
122 count += add_directory_to_tar(builder, &path, &archive_path)?;
124 } else if path.is_symlink() {
125 let link_target = std::fs::read_link(&path)
127 .map_err(|e| Error::Bottle(format!("Failed to read symlink: {}", e)))?;
128
129 let mut header = tar::Header::new_gnu();
130 header.set_entry_type(tar::EntryType::Symlink);
131 header.set_size(0);
132
133 if let Ok(metadata) = std::fs::symlink_metadata(&path) {
135 header.set_mode(get_mode(&metadata));
136 header.set_mtime(
137 metadata
138 .modified()
139 .ok()
140 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
141 .map(|d| d.as_secs())
142 .unwrap_or(0),
143 );
144 }
145
146 builder
147 .append_link(&mut header, &archive_path, &link_target)
148 .map_err(|e| Error::Bottle(format!("Failed to add symlink to archive: {}", e)))?;
149
150 debug!("Added symlink: {} -> {:?}", archive_path, link_target);
151 count += 1;
152 } else if path.is_file() {
153 let metadata = std::fs::metadata(&path)
155 .map_err(|e| Error::Bottle(format!("Failed to get file metadata: {}", e)))?;
156
157 let mut header = tar::Header::new_gnu();
158 header.set_size(metadata.len());
159 header.set_mode(get_mode(&metadata));
160 header.set_mtime(
161 metadata
162 .modified()
163 .ok()
164 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
165 .map(|d| d.as_secs())
166 .unwrap_or(0),
167 );
168
169 let mut file = File::open(&path)
170 .map_err(|e| Error::Bottle(format!("Failed to open file: {}", e)))?;
171
172 builder
173 .append_data(&mut header, &archive_path, &mut file)
174 .map_err(|e| Error::Bottle(format!("Failed to add file to archive: {}", e)))?;
175
176 debug!("Added file: {}", archive_path);
177 count += 1;
178 }
179 }
180
181 Ok(count)
182}
183
184fn get_mode(metadata: &std::fs::Metadata) -> u32 {
186 #[cfg(unix)]
187 {
188 use std::os::unix::fs::PermissionsExt;
189 metadata.permissions().mode()
190 }
191 #[cfg(not(unix))]
192 {
193 if metadata.is_dir() {
194 0o755
195 } else {
196 0o644
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use std::fs;
205 use tempfile::TempDir;
206
207 #[test]
208 fn test_create_bottle() {
209 let temp_dir = TempDir::new().unwrap();
210 let install_path = temp_dir.path().join("test-pkg/1.0.0");
211 let bin_dir = install_path.join("bin");
212 fs::create_dir_all(&bin_dir).unwrap();
213
214 fs::write(bin_dir.join("test-binary"), b"#!/bin/sh\necho hello").unwrap();
216
217 let bottle_path = temp_dir.path().join("test-pkg-1.0.0.bottle.tar.gz");
218
219 let result = create_bottle(&install_path, &bottle_path, "test-pkg", "1.0.0").unwrap();
220
221 assert!(result.path.exists());
222 assert!(result.size > 0);
223 assert!(!result.sha256.is_empty());
224 assert!(result.file_count > 0);
225 }
226}