1use std::{
2 collections::BTreeMap,
3 fs,
4 os::unix,
5 path::{Path, PathBuf},
6};
7
8use anyhow::Context;
9use data_encoding::HEXLOWER;
10use flate2::read::GzDecoder;
11use reqwest::blocking::Response;
12use serde::Deserialize;
13
14use crate::{
15 cache::http_client,
16 db::models::{DownloadedBottle, LinkedFile},
17 dirs,
18 formulae::Formula,
19 macho, magic,
20 validate::Validate,
21};
22
23#[derive(Debug, Deserialize)]
24pub struct Bottles {
25 pub stable: Bottle,
26}
27
28#[derive(Debug, Deserialize)]
29pub struct Bottle {
30 pub files: BTreeMap<String, FileMetadata>,
31}
32
33#[derive(Debug, Deserialize)]
34pub struct FileMetadata {
35 pub url: String,
36 pub sha256: String,
37}
38
39impl Formula {
40 pub fn download_bottle(&self) -> anyhow::Result<DownloadedBottle> {
41 if let Some(bottle) = DownloadedBottle::get(&self.name, &self.versions.stable)? {
42 return Ok(bottle);
43 }
44
45 println!("Dowloading {} {}...", self.name, self.versions.stable);
46 let result = self.download_bottle_inner();
47
48 if result.is_err() {
49 if let Ok(Some(path)) = self.bottle_path() {
50 let _ = fs::remove_dir_all(&path);
51 if let Some(parent) = path.parent() {
52 let _ = fs::remove_dir(parent);
53 }
54 }
55 }
56
57 result.with_context(|| format!("Downloading {} {}", self.name, self.versions.stable))
58 }
59
60 fn download_bottle_inner(&self) -> anyhow::Result<DownloadedBottle> {
63 let bottles_dir = dirs::bottles_dir()?;
64 let file_metadata = self.bottle.stable.current_target()?;
65
66 let mut raw_data = file_metadata
67 .fetch()
68 .context("Failed to fetch bottle archive")?;
69 let unzip = GzDecoder::new(&mut raw_data);
70 let mut tar = tar::Archive::new(unzip);
71 tar.unpack(bottles_dir)
72 .context("Failed to unpack bottle archive")?;
73
74 let path = self
75 .bottle_path()?
76 .context("Failed to find where bottle was extracted to")?;
77
78 raw_data
79 .validate()
80 .context("Failed to validate bottle download")?;
81
82 let bottle = DownloadedBottle::create(&self.name, &self.versions.stable, &path)?;
83
84 bottle.patch().context("Failed to patch bottle")?;
85
86 Ok(bottle)
87 }
88
89 pub fn bottle_path(&self) -> anyhow::Result<Option<PathBuf>> {
90 let name = self.name.as_str();
91 let version = self.versions.stable.as_str();
92
93 let bottles_path = dirs::bottles_dir()?;
94 let parent_path = bottles_path.join(name);
95 if !parent_path.exists() {
96 return Ok(None);
97 }
98
99 let path = parent_path.join(version);
100 if path.exists() {
101 return Ok(Some(path));
102 }
103
104 for child in fs::read_dir(parent_path)? {
106 let child = child?;
107 let file_name = child.file_name();
108 let file_name = file_name.to_str().context("Invalid file name")?;
109 if file_name.starts_with(version) && file_name[version.len()..].starts_with('_') {
110 return Ok(Some(child.path()));
111 }
112 }
113
114 Ok(None)
115 }
116}
117
118impl Bottle {
119 pub fn current_target(&self) -> anyhow::Result<&FileMetadata> {
120 let target = crate::target::Target::current_str()?;
121 if let Some(file) = self.files.get(target) {
122 Ok(file)
123 } else if let Some(file) = self.files.get("all") {
124 Ok(file)
125 } else {
126 anyhow::bail!("No bottle for target: {target}");
127 }
128 }
129}
130
131impl FileMetadata {
132 pub fn fetch(&self) -> anyhow::Result<Validate<Response>> {
133 let response = http_client()
134 .get(&self.url)
135 .bearer_auth("QQ==")
137 .send()?;
138 anyhow::ensure!(
139 response.status().is_success(),
140 "Failed to fetch bottle. Response code was: {}",
141 response.status(),
142 );
143
144 let sha256 = HEXLOWER.decode(self.sha256.as_bytes())?;
145 let reader = Validate::new(response, sha256);
146
147 Ok(reader)
148 }
149}
150
151impl DownloadedBottle {
152 pub fn patch(&self) -> anyhow::Result<()> {
153 fn traverse(path: &Path) -> anyhow::Result<()> {
154 let stat = path
155 .symlink_metadata()
156 .with_context(|| format!("Failed to get metadata for {}", path.display()))?;
157 if stat.is_dir() {
158 for entry in fs::read_dir(path)? {
159 let entry = entry?;
160 traverse(&entry.path())?;
161 }
162 } else if stat.is_file() {
163 match magic::detect(path).unwrap_or(magic::Magic::Unknown) {
164 #[cfg(target_os = "macos")]
165 magic::Magic::MachO => macho::patch(path)?,
166 #[cfg(target_os = "linux")]
167 magic::Magic::Elf => todo!(),
168 _ => (),
169 }
170 }
171
172 Ok(())
173 }
174
175 println!("Patching {} {}...", self.name(), self.version());
176
177 traverse(self.path())?;
178
179 Ok(())
180 }
181
182 pub fn link(&self) -> anyhow::Result<()> {
183 println!("Linking {} {}...", self.name(), self.version());
184
185 let bin_dir = dirs::bin_dir()?;
186 let bottle_bin_dir = PathBuf::from(self.path()).join("bin");
187
188 if bottle_bin_dir.exists() {
189 for entry in fs::read_dir(bottle_bin_dir)? {
190 let entry = entry?;
191 let entry_path = entry.path();
192 let entry_name = entry.file_name();
193 let dest = bin_dir.join(entry_name);
194
195 if dest.exists() {
196 let Ok(existing_path) = fs::read_link(&dest) else {
197 continue;
198 };
199 if !existing_path.starts_with(dirs::bottles_dir()?) {
200 continue;
201 }
202 fs::remove_file(&dest)?;
203 }
204
205 LinkedFile::create(&dest, self)?;
206
207 unix::fs::symlink(&entry_path, &dest)?;
208 }
209 }
210
211 Ok(())
212 }
213
214 pub fn unlink(&self) -> anyhow::Result<()> {
215 println!("Unlinking {} {}...", self.name(), self.version());
216
217 let bottle_dir = self.path();
218 for linked_file in self.linked_files()? {
219 if let Ok(linked_path) = fs::read_link(linked_file.path()) {
220 if linked_path.starts_with(bottle_dir) {
221 fs::remove_file(linked_file.path())?;
222 }
223 }
224
225 linked_file.delete()?;
226 }
227
228 Ok(())
229 }
230
231 pub fn remove(&self) -> anyhow::Result<()> {
232 println!("Deleting {} {}...", self.name(), self.version());
233
234 let _ = fs::remove_dir_all(self.path());
235 if let Some(parent) = self.path().parent() {
236 let _ = fs::remove_dir(parent);
237 }
238
239 self.delete()?;
240
241 Ok(())
242 }
243}