1use crate::build::utils::recursive_copy_dir;
2use crate::config::Config;
3use crate::git::url::RemoteGitUrlParseError;
4use crate::git::GitSource;
5use crate::hash::HasIntegrity;
6use crate::lockfile::RemotePackageSourceUrl;
7use crate::lua_rockspec::RockSourceSpec;
8use crate::operations;
9use crate::package::PackageSpec;
10use crate::rockspec::Rockspec;
11use auth_git2::{GitAuthenticator, Prompter};
12use bon::Builder;
13use git2::build::RepoBuilder;
14use git2::{FetchOptions, RemoteCallbacks};
15use miette::Diagnostic;
16use remove_dir_all::remove_dir_all;
17use ssri::Integrity;
18use std::fs::File;
19use std::io;
20use std::io::Cursor;
21use std::io::Read;
22use std::path::Path;
23use std::path::PathBuf;
24use thiserror::Error;
25use tracing::span;
26
27use super::DownloadSrcRockError;
28use super::UnpackError;
29
30#[derive(Builder)]
33#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
34pub struct FetchSrc<'a, R: Rockspec> {
35 #[builder(start_fn)]
36 dest_dir: &'a Path,
37 #[builder(start_fn)]
38 rockspec: &'a R,
39 #[builder(start_fn)]
40 config: &'a Config,
41 #[builder(setters(vis = "pub(crate)"))]
42 source_url: Option<RemotePackageSourceUrl>,
43}
44
45#[derive(Debug)]
46pub(crate) struct RemotePackageSourceMetadata {
47 pub hash: Integrity,
48 pub source_url: RemotePackageSourceUrl,
49}
50
51impl<R: Rockspec, State> FetchSrcBuilder<'_, R, State>
52where
53 State: fetch_src_builder::State + fetch_src_builder::IsComplete,
54{
55 pub async fn fetch(self) -> Result<(), FetchSrcError> {
57 self.fetch_internal().await?;
58 Ok(())
59 }
60
61 pub(crate) async fn fetch_internal(self) -> Result<RemotePackageSourceMetadata, FetchSrcError> {
64 let fetch = self._build();
65 match do_fetch_src(&fetch).await {
66 Err(err)
67 if fetch
68 .source_url
69 .is_some_and(|url| matches!(url, RemotePackageSourceUrl::File { .. })) =>
70 {
71 Err(err)
73 }
74 Err(err) => match &fetch.rockspec.source().current_platform().source_spec {
75 RockSourceSpec::Git(_) | RockSourceSpec::Url(_) => {
76 let package = PackageSpec::new(
77 fetch.rockspec.package().clone(),
78 fetch.rockspec.version().clone(),
79 );
80 let metadata = FetchSrcRock::new(&package, fetch.dest_dir, fetch.config)
81 .fetch()
82 .await?;
83 Ok(metadata)
84 }
85 RockSourceSpec::File(_) => Err(err),
86 },
87 Ok(metadata) => Ok(metadata),
88 }
89 }
90}
91
92#[derive(Error, Debug, Diagnostic)]
93#[non_exhaustive]
94pub enum FetchSrcError {
95 #[error("failed to clone rock source:\n{0}")]
96 #[diagnostic(help("check your network connection and verify the git URL is correct."))]
97 GitClone(#[from] git2::Error),
98 #[error("failed to parse git URL:\n{0}")]
99 #[diagnostic(forward(0))]
100 GitUrlParse(#[from] RemoteGitUrlParseError),
101 #[error(transparent)]
102 #[diagnostic(help("check your network connection."))]
103 Request(#[from] reqwest::Error),
104 #[error(transparent)]
105 #[diagnostic(transparent)]
106 Unpack(#[from] UnpackError),
107 #[error(transparent)]
108 #[diagnostic(transparent)]
109 FetchSrcRock(#[from] FetchSrcRockError),
110 #[error("unable to remove the '.git' directory:\n{0}")]
111 #[diagnostic(help(
112 "check that no process is using the directory and you have write permissions."
113 ))]
114 CleanGitDir(io::Error),
115 #[error("unable to compute hash:\n{0}")]
116 Hash(io::Error),
117 #[error("unable to copy {src} to {dest}:\n{err}")]
118 #[diagnostic(help("check that the source exists and the destination is writable."))]
119 CopyDir {
120 src: PathBuf,
121 dest: PathBuf,
122 err: io::Error,
123 },
124 #[error("unable to open {file}:\n{err}")]
125 #[diagnostic(help("check that the file exists and is readable."))]
126 FileOpen { file: PathBuf, err: io::Error },
127 #[error("unable to read {file}:\n{err}")]
128 #[diagnostic(help("check that the file exists and is readable."))]
129 FileRead { file: PathBuf, err: io::Error },
130}
131
132#[derive(Builder)]
135#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
136struct FetchSrcRock<'a> {
137 #[builder(start_fn)]
138 package: &'a PackageSpec,
139 #[builder(start_fn)]
140 dest_dir: &'a Path,
141 #[builder(start_fn)]
142 config: &'a Config,
143}
144
145impl<State> FetchSrcRockBuilder<'_, State>
146where
147 State: fetch_src_rock_builder::State + fetch_src_rock_builder::IsComplete,
148{
149 pub async fn fetch(self) -> Result<RemotePackageSourceMetadata, FetchSrcRockError> {
150 do_fetch_src_rock(self._build()).await
151 }
152}
153
154#[derive(Error, Debug, Diagnostic)]
155#[non_exhaustive]
156#[error(transparent)]
157pub enum FetchSrcRockError {
158 DownloadSrcRock(#[from] DownloadSrcRockError),
159 Unpack(#[from] UnpackError),
160 Io(#[from] io::Error),
161}
162
163#[derive(Copy, Clone, Debug)]
165struct NullPrompter;
166
167impl Prompter for NullPrompter {
168 fn prompt_username_password(&mut self, _: &str, _: &git2::Config) -> Option<(String, String)> {
169 None
170 }
171
172 fn prompt_password(&mut self, _: &str, _: &str, _: &git2::Config) -> Option<String> {
173 None
174 }
175
176 fn prompt_ssh_key_passphrase(&mut self, _: &Path, _: &git2::Config) -> Option<String> {
177 None
178 }
179}
180
181async fn do_fetch_src<R: Rockspec>(
182 fetch: &FetchSrc<'_, R>,
183) -> Result<RemotePackageSourceMetadata, FetchSrcError> {
184 let rockspec = fetch.rockspec;
185 let rock_source = rockspec.source().current_platform();
186 let dest_dir = fetch.dest_dir;
187 let config = fetch.config;
188 let mut source_spec = match &fetch.source_url {
190 Some(source_url) => match source_url {
191 RemotePackageSourceUrl::Git { url, checkout_ref } => RockSourceSpec::Git(GitSource {
192 url: url.parse()?,
193 checkout_ref: Some(checkout_ref.clone()),
194 }),
195 RemotePackageSourceUrl::Url { url } => RockSourceSpec::Url(url.clone()),
196 RemotePackageSourceUrl::File { path } => RockSourceSpec::File(path.clone()),
197 },
198 None => rock_source.source_spec.clone(),
199 };
200 let span = span!(
201 tracing::Level::INFO,
202 "Fetching source",
203 location = source_spec.to_string(),
204 );
205 let _enter = span.enter();
206
207 if let Some(vendor_dir) = config.vendor_dir() {
208 source_spec = match source_spec {
209 RockSourceSpec::File(_) => source_spec,
212 _ => {
213 let pkg_vendor_dir =
214 vendor_dir.join(format!("{}@{}", rockspec.package(), rockspec.version()));
215 RockSourceSpec::File(pkg_vendor_dir)
216 }
217 }
218 }
219 let metadata = match &source_spec {
220 RockSourceSpec::Git(git) => {
221 let url = git.url.to_string();
222 tracing::debug!(message = format!("Cloning {url}").as_str());
223
224 let auth = if config.no_prompt() {
225 GitAuthenticator::default()
226 .try_password_prompt(0)
227 .prompt_ssh_key_password(false)
228 .set_prompter(NullPrompter)
229 } else {
230 GitAuthenticator::default()
231 };
232 let git_config = git2::Config::open_default()?;
233 let mut callbacks = RemoteCallbacks::new();
234 callbacks.credentials(auth.credentials(&git_config));
235 let mut fetch_options = FetchOptions::new();
236 fetch_options.update_fetchhead(false);
237 fetch_options.remote_callbacks(callbacks);
238 if git.checkout_ref.is_none() {
239 fetch_options.depth(1);
240 };
241 let mut repo_builder = RepoBuilder::new();
242 repo_builder.fetch_options(fetch_options);
243 let repo = repo_builder.clone(&url, dest_dir)?;
244
245 let checkout_ref = match &git.checkout_ref {
246 Some(checkout_ref) => {
247 let (object, _) = repo.revparse_ext(checkout_ref)?;
248 repo.checkout_tree(&object, None)?;
249 checkout_ref.clone()
250 }
251 None => {
252 let head = repo.head()?;
253 let commit = head.peel_to_commit()?;
254 commit.id().to_string()
255 }
256 };
257 remove_dir_all(dest_dir.join(".git")).map_err(FetchSrcError::CleanGitDir)?;
259 let hash = fetch.dest_dir.hash().map_err(FetchSrcError::Hash)?;
260 RemotePackageSourceMetadata {
261 hash,
262 source_url: RemotePackageSourceUrl::Git { url, checkout_ref },
263 }
264 }
265 RockSourceSpec::Url(url) => {
266 tracing::debug!(message = format!("📥 Downloading {url}").as_str());
267
268 let response = crate::reqwest::new_http_client(config)?
271 .get(url.clone())
272 .send()
273 .await?
274 .error_for_status()?
275 .bytes()
276 .await?;
277 let hash = response.hash().map_err(FetchSrcError::Hash)?;
278 let file_name = url
279 .path_segments()
280 .and_then(|mut segments| segments.next_back())
281 .and_then(|name| {
282 if name.is_empty() {
283 None
284 } else {
285 Some(name.to_string())
286 }
287 })
288 .unwrap_or(url.to_string());
289 let cursor = Cursor::new(response);
290 let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
291 operations::unpack::unpack(
292 mime_type,
293 cursor,
294 rock_source.unpack_dir.is_none(),
295 file_name,
296 dest_dir,
297 )
298 .await?;
299 RemotePackageSourceMetadata {
300 hash,
301 source_url: RemotePackageSourceUrl::Url { url: url.clone() },
302 }
303 }
304 RockSourceSpec::File(path) => {
305 tracing::debug!(message = format!("📋 Copying {}", path.display()).as_str());
306
307 let hash = if path.is_dir() {
308 recursive_copy_dir(&path.to_path_buf(), dest_dir)
309 .await
310 .map_err(|err| FetchSrcError::CopyDir {
311 src: path.to_path_buf(),
312 dest: dest_dir.to_path_buf(),
313 err,
314 })?;
315 dest_dir.hash().map_err(FetchSrcError::Hash)?
316 } else {
317 let mut file = File::open(path).map_err(|err| FetchSrcError::FileOpen {
318 file: path.clone(),
319 err,
320 })?;
321 let mut buffer = Vec::new();
322 file.read_to_end(&mut buffer)
323 .map_err(|err| FetchSrcError::FileRead {
324 file: path.clone(),
325 err,
326 })?;
327 let mime_type = infer::get(&buffer).map(|file_type| file_type.mime_type());
328 let file_name = path
329 .file_name()
330 .map(|os_str| os_str.to_string_lossy())
331 .unwrap_or(path.to_string_lossy())
332 .to_string();
333 operations::unpack::unpack(
334 mime_type,
335 file,
336 rock_source.unpack_dir.is_none(),
337 file_name,
338 dest_dir,
339 )
340 .await?;
341 path.hash().map_err(FetchSrcError::Hash)?
342 };
343 RemotePackageSourceMetadata {
344 hash,
345 source_url: RemotePackageSourceUrl::File { path: path.clone() },
346 }
347 }
348 };
349 Ok(metadata)
350}
351
352async fn do_fetch_src_rock(
353 fetch: FetchSrcRock<'_>,
354) -> Result<RemotePackageSourceMetadata, FetchSrcRockError> {
355 let package = fetch.package;
356 let span = span!(
357 tracing::Level::INFO,
358 "Fetching src.rock",
359 package = package.to_string(),
360 );
361 let _enter = span.enter();
362
363 let dest_dir = fetch.dest_dir;
364 let config = fetch.config;
365 let src_rock = operations::download_src_rock(package, config.server(), fetch.config).await?;
366 let hash = src_rock.bytes.hash()?;
367 let cursor = Cursor::new(src_rock.bytes);
368 let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
369 operations::unpack::unpack(mime_type, cursor, true, src_rock.file_name, dest_dir).await?;
370 Ok(RemotePackageSourceMetadata {
371 hash,
372 source_url: RemotePackageSourceUrl::Url { url: src_rock.url },
373 })
374}