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