1use bon::Builder;
2use git2::build::RepoBuilder;
3use git2::FetchOptions;
4use git_url_parse::GitUrlParseError;
5use ssri::Integrity;
6use std::fs::File;
7use std::io;
8use std::io::Cursor;
9use std::io::Read;
10use std::path::Path;
11use thiserror::Error;
12
13use crate::build::utils::recursive_copy_dir;
14use crate::config::Config;
15use crate::git::GitSource;
16use crate::hash::HasIntegrity;
17use crate::lockfile::RemotePackageSourceUrl;
18use crate::lua_rockspec::RockSourceSpec;
19use crate::operations;
20use crate::package::PackageSpec;
21use crate::progress::Progress;
22use crate::progress::ProgressBar;
23use crate::rockspec::Rockspec;
24
25use super::DownloadSrcRockError;
26use super::UnpackError;
27
28#[derive(Builder)]
31#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
32pub struct FetchSrc<'a, R: Rockspec> {
33 #[builder(start_fn)]
34 dest_dir: &'a Path,
35 #[builder(start_fn)]
36 rockspec: &'a R,
37 #[builder(start_fn)]
38 config: &'a Config,
39 #[builder(start_fn)]
40 progress: &'a Progress<ProgressBar>,
41 source_url: Option<RemotePackageSourceUrl>,
42}
43
44#[derive(Debug)]
45pub(crate) struct RemotePackageSourceMetadata {
46 pub hash: Integrity,
47 pub source_url: RemotePackageSourceUrl,
48}
49
50impl<R: Rockspec, State> FetchSrcBuilder<'_, R, State>
51where
52 State: fetch_src_builder::State + fetch_src_builder::IsComplete,
53{
54 pub async fn fetch(self) -> Result<(), FetchSrcError> {
56 self.fetch_internal().await?;
57 Ok(())
58 }
59
60 pub(crate) async fn fetch_internal(self) -> Result<RemotePackageSourceMetadata, FetchSrcError> {
63 let fetch = self._build();
64 match do_fetch_src(&fetch).await {
65 Err(err) => match &fetch.rockspec.source().current_platform().source_spec {
66 RockSourceSpec::Git(_) | RockSourceSpec::Url(_) => {
67 let package = PackageSpec::new(
68 fetch.rockspec.package().clone(),
69 fetch.rockspec.version().clone(),
70 );
71 fetch.progress.map(|p| {
72 p.println(format!(
73 "⚠️ WARNING: Failed to fetch source for {}: {}",
74 &package, err
75 ))
76 });
77 fetch
78 .progress
79 .map(|p| p.println("⚠️ Falling back to .src.rock archive"));
80 let metadata =
81 FetchSrcRock::new(&package, fetch.dest_dir, fetch.config, fetch.progress)
82 .fetch()
83 .await?;
84 Ok(metadata)
85 }
86 RockSourceSpec::File(_) => Err(err),
87 },
88 Ok(metadata) => Ok(metadata),
89 }
90 }
91}
92
93#[derive(Error, Debug)]
94pub enum FetchSrcError {
95 #[error("failed to clone rock source: {0}")]
96 GitClone(#[from] git2::Error),
97 #[error("failed to parse git URL: {0}")]
98 GitUrlParse(#[from] GitUrlParseError),
99 #[error(transparent)]
100 Io(#[from] io::Error),
101 #[error(transparent)]
102 Request(#[from] reqwest::Error),
103 #[error(transparent)]
104 Unpack(#[from] UnpackError),
105 #[error(transparent)]
106 FetchSrcRock(#[from] FetchSrcRockError),
107}
108
109#[derive(Builder)]
112#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
113struct FetchSrcRock<'a> {
114 #[builder(start_fn)]
115 package: &'a PackageSpec,
116 #[builder(start_fn)]
117 dest_dir: &'a Path,
118 #[builder(start_fn)]
119 config: &'a Config,
120 #[builder(start_fn)]
121 progress: &'a Progress<ProgressBar>,
122}
123
124impl<State> FetchSrcRockBuilder<'_, State>
125where
126 State: fetch_src_rock_builder::State + fetch_src_rock_builder::IsComplete,
127{
128 pub async fn fetch(self) -> Result<RemotePackageSourceMetadata, FetchSrcRockError> {
129 do_fetch_src_rock(self._build()).await
130 }
131}
132
133#[derive(Error, Debug)]
134#[error(transparent)]
135pub enum FetchSrcRockError {
136 DownloadSrcRock(#[from] DownloadSrcRockError),
137 Unpack(#[from] UnpackError),
138 Io(#[from] io::Error),
139}
140
141async fn do_fetch_src<R: Rockspec>(
142 fetch: &FetchSrc<'_, R>,
143) -> Result<RemotePackageSourceMetadata, FetchSrcError> {
144 let rockspec = fetch.rockspec;
145 let rock_source = rockspec.source().current_platform();
146 let progress = fetch.progress;
147 let dest_dir = fetch.dest_dir;
148 let source_spec = match &fetch.source_url {
150 Some(source_url) => match source_url {
151 RemotePackageSourceUrl::Git { url, checkout_ref } => RockSourceSpec::Git(GitSource {
152 url: url.parse()?,
153 checkout_ref: Some(checkout_ref.clone()),
154 }),
155 RemotePackageSourceUrl::Url { url } => RockSourceSpec::Url(url.clone()),
156 RemotePackageSourceUrl::File { path } => RockSourceSpec::File(path.clone()),
157 },
158 None => rock_source.source_spec.clone(),
159 };
160 let metadata = match &source_spec {
161 RockSourceSpec::Git(git) => {
162 let url = git.url.to_string();
163 progress.map(|p| p.set_message(format!("🦠 Cloning {}", url)));
164
165 let mut fetch_options = FetchOptions::new();
166 fetch_options.update_fetchhead(false);
167 if git.checkout_ref.is_none() {
168 fetch_options.depth(1);
169 };
170 let mut repo_builder = RepoBuilder::new();
171 repo_builder.fetch_options(fetch_options);
172 let repo = repo_builder.clone(&url, dest_dir)?;
173
174 let checkout_ref = match &git.checkout_ref {
175 Some(checkout_ref) => {
176 let (object, _) = repo.revparse_ext(checkout_ref)?;
177 repo.checkout_tree(&object, None)?;
178 checkout_ref.clone()
179 }
180 None => {
181 let head = repo.head()?;
182 let commit = head.peel_to_commit()?;
183 commit.id().to_string()
184 }
185 };
186 std::fs::remove_dir_all(dest_dir.join(".git"))?;
188 let hash = fetch.dest_dir.hash()?;
189 RemotePackageSourceMetadata {
190 hash,
191 source_url: RemotePackageSourceUrl::Git { url, checkout_ref },
192 }
193 }
194 RockSourceSpec::Url(url) => {
195 progress.map(|p| p.set_message(format!("📥 Downloading {}", url.to_owned())));
196
197 let response = reqwest::get(url.to_owned())
198 .await?
199 .error_for_status()?
200 .bytes()
201 .await?;
202 let hash = response.hash()?;
203 let file_name = url
204 .path_segments()
205 .and_then(|mut segments| segments.next_back())
206 .and_then(|name| {
207 if name.is_empty() {
208 None
209 } else {
210 Some(name.to_string())
211 }
212 })
213 .unwrap_or(url.to_string());
214 let cursor = Cursor::new(response);
215 let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
216 operations::unpack::unpack(
217 mime_type,
218 cursor,
219 rock_source.unpack_dir.is_none(),
220 file_name,
221 dest_dir,
222 progress,
223 )
224 .await?;
225 RemotePackageSourceMetadata {
226 hash,
227 source_url: RemotePackageSourceUrl::Url { url: url.clone() },
228 }
229 }
230 RockSourceSpec::File(path) => {
231 let hash = if path.is_dir() {
232 progress.map(|p| p.set_message(format!("📋 Copying {}", path.display())));
233 recursive_copy_dir(&path.to_path_buf(), dest_dir).await?;
234 progress.map(|p| p.finish_and_clear());
235 dest_dir.hash()?
236 } else {
237 let mut file = File::open(path)?;
238 let mut buffer = Vec::new();
239 file.read_to_end(&mut buffer)?;
240 let mime_type = infer::get(&buffer).map(|file_type| file_type.mime_type());
241 let file_name = path
242 .file_name()
243 .map(|os_str| os_str.to_string_lossy())
244 .unwrap_or(path.to_string_lossy())
245 .to_string();
246 operations::unpack::unpack(
247 mime_type,
248 file,
249 rock_source.unpack_dir.is_none(),
250 file_name,
251 dest_dir,
252 progress,
253 )
254 .await?;
255 path.hash()?
256 };
257 RemotePackageSourceMetadata {
258 hash,
259 source_url: RemotePackageSourceUrl::File { path: path.clone() },
260 }
261 }
262 };
263 Ok(metadata)
264}
265
266async fn do_fetch_src_rock(
267 fetch: FetchSrcRock<'_>,
268) -> Result<RemotePackageSourceMetadata, FetchSrcRockError> {
269 let package = fetch.package;
270 let dest_dir = fetch.dest_dir;
271 let config = fetch.config;
272 let progress = fetch.progress;
273 let src_rock = operations::download_src_rock(package, config.server(), progress).await?;
274 let hash = src_rock.bytes.hash()?;
275 let cursor = Cursor::new(src_rock.bytes);
276 let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
277 operations::unpack::unpack(
278 mime_type,
279 cursor,
280 true,
281 src_rock.file_name,
282 dest_dir,
283 progress,
284 )
285 .await?;
286 Ok(RemotePackageSourceMetadata {
287 hash,
288 source_url: RemotePackageSourceUrl::Url { url: src_rock.url },
289 })
290}