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::{RemoteRockSource, RockSourceSpec};
8use crate::package::PackageSpec;
9use crate::rockspec::Rockspec;
10use crate::{fs, operations};
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::io;
19use std::io::Cursor;
20use std::io::Read;
21use std::path::Path;
22use thiserror::Error;
23
24use super::DownloadSrcRockError;
25use super::UnpackError;
26
27#[derive(Builder)]
30#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
31pub struct FetchSrc<'a, R: Rockspec> {
32 #[builder(start_fn)]
33 dest_dir: &'a Path,
34 #[builder(start_fn)]
35 rockspec: &'a R,
36 #[builder(start_fn)]
37 config: &'a Config,
38 #[builder(setters(vis = "pub(crate)"))]
39 source_url: Option<RemotePackageSourceUrl>,
40}
41
42#[derive(Debug)]
43pub(crate) struct RemotePackageSourceMetadata {
44 pub hash: Integrity,
45 pub source_url: RemotePackageSourceUrl,
46}
47
48impl<R: Rockspec, State> FetchSrcBuilder<'_, R, State>
49where
50 State: fetch_src_builder::State + fetch_src_builder::IsComplete,
51{
52 pub async fn fetch(self) -> Result<(), FetchSrcError> {
54 self.fetch_internal().await?;
55 Ok(())
56 }
57
58 pub(crate) async fn fetch_internal(self) -> Result<RemotePackageSourceMetadata, FetchSrcError> {
61 let fetch = self._build();
62 let rockspec = fetch.rockspec;
63 let rock_source = rockspec.source().current_platform();
64 let dest_dir = fetch.dest_dir;
65 let config = fetch.config;
66 let source_spec = match &fetch.source_url {
68 Some(source_url) => match source_url {
69 RemotePackageSourceUrl::Git { url, checkout_ref } => {
70 RockSourceSpec::Git(GitSource {
71 url: url.parse()?,
72 checkout_ref: Some(checkout_ref.clone()),
73 })
74 }
75 RemotePackageSourceUrl::Url { url } => RockSourceSpec::Url(url.clone()),
76 RemotePackageSourceUrl::File { path } => RockSourceSpec::File(path.clone()),
77 },
78 None => rock_source.source_spec.clone(),
79 };
80 match fetch_src_impl(source_spec, rockspec, rock_source, dest_dir, config).await {
81 Err(err)
82 if fetch
83 .source_url
84 .is_some_and(|url| matches!(url, RemotePackageSourceUrl::File { .. })) =>
85 {
86 Err(err)
88 }
89 Err(err) => match &fetch.rockspec.source().current_platform().source_spec {
90 RockSourceSpec::Git(_) | RockSourceSpec::Url(_) => {
91 let package = PackageSpec::new(
92 fetch.rockspec.package().clone(),
93 fetch.rockspec.version().clone(),
94 );
95 match FetchSrcRock::new(&package, fetch.dest_dir, fetch.config)
96 .fetch()
97 .await
98 {
99 Ok(metadata) => Ok(metadata),
100 Err(fallback_err) => {
101 tracing::error!("fallback .src.rock download failed: {fallback_err:?}");
102 Err(err)
103 }
104 }
105 }
106 RockSourceSpec::File(_) => Err(err),
107 },
108 Ok(metadata) => Ok(metadata),
109 }
110 }
111}
112
113#[derive(Error, Debug, Diagnostic)]
114#[non_exhaustive]
115pub enum FetchSrcError {
116 #[error("failed to clone rock source")]
117 #[diagnostic(help("check your network connection and verify the git URL is correct."))]
118 GitClone(#[from] git2::Error),
119 #[error("failed to parse git URL")]
120 #[diagnostic(forward(0))]
121 GitUrlParse(#[from] RemoteGitUrlParseError),
122 #[error(transparent)]
123 #[diagnostic(help("check your network connection."))]
124 Request(#[from] reqwest::Error),
125 #[error(transparent)]
126 #[diagnostic(transparent)]
127 Unpack(#[from] UnpackError),
128 #[error(transparent)]
129 #[diagnostic(transparent)]
130 FetchSrcRock(#[from] FetchSrcRockError),
131 #[error("unable to remove the '.git' directory")]
132 #[diagnostic(help(
133 "check that no process is using the directory and you have write permissions."
134 ))]
135 CleanGitDir(#[source] io::Error),
136 #[error("unable to compute hash")]
137 Hash(#[source] io::Error),
138 #[error(transparent)]
139 #[diagnostic(transparent)]
140 Fs(#[from] fs::FsError),
141}
142
143#[derive(Builder)]
146#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
147struct FetchSrcRock<'a> {
148 #[builder(start_fn)]
149 package: &'a PackageSpec,
150 #[builder(start_fn)]
151 dest_dir: &'a Path,
152 #[builder(start_fn)]
153 config: &'a Config,
154}
155
156impl<State> FetchSrcRockBuilder<'_, State>
157where
158 State: fetch_src_rock_builder::State + fetch_src_rock_builder::IsComplete,
159{
160 pub async fn fetch(self) -> Result<RemotePackageSourceMetadata, FetchSrcRockError> {
161 do_fetch_src_rock(self._build()).await
162 }
163}
164
165#[derive(Error, Debug, Diagnostic)]
166#[non_exhaustive]
167#[error(transparent)]
168pub enum FetchSrcRockError {
169 DownloadSrcRock(#[from] DownloadSrcRockError),
170 Unpack(#[from] UnpackError),
171 Io(#[from] io::Error),
172}
173
174#[derive(Copy, Clone, Debug)]
176struct NullPrompter;
177
178impl Prompter for NullPrompter {
179 fn prompt_username_password(&mut self, _: &str, _: &git2::Config) -> Option<(String, String)> {
180 None
181 }
182
183 fn prompt_password(&mut self, _: &str, _: &str, _: &git2::Config) -> Option<String> {
184 None
185 }
186
187 fn prompt_ssh_key_passphrase(&mut self, _: &Path, _: &git2::Config) -> Option<String> {
188 None
189 }
190}
191
192#[tracing::instrument(
193 name = "Fetching source",
194 level = "info",
195 skip_all,
196 fields(location = source_spec.to_string()),
197)]
198async fn fetch_src_impl<R: Rockspec>(
199 mut source_spec: RockSourceSpec,
200 rockspec: &R,
201 rock_source: &RemoteRockSource,
202 dest_dir: &Path,
203 config: &Config,
204) -> Result<RemotePackageSourceMetadata, FetchSrcError> {
205 if let Some(vendor_dir) = config.vendor_dir() {
206 source_spec = match source_spec {
207 RockSourceSpec::File(_) => source_spec,
210 _ => {
211 let pkg_vendor_dir =
212 vendor_dir.join(format!("{}@{}", rockspec.package(), rockspec.version()));
213 RockSourceSpec::File(pkg_vendor_dir)
214 }
215 }
216 }
217 let metadata = match &source_spec {
218 RockSourceSpec::Git(git) => {
219 let url = git.url.to_string();
220 tracing::debug!(message = format!("Cloning {url}").as_str());
221
222 let checkout_ref = {
223 let auth = if config.no_prompt() {
224 GitAuthenticator::default()
225 .try_password_prompt(0)
226 .prompt_ssh_key_password(false)
227 .set_prompter(NullPrompter)
228 } else {
229 GitAuthenticator::default()
230 };
231 let git_config = git2::Config::open_default()?;
232 let mut callbacks = RemoteCallbacks::new();
233 callbacks.credentials(auth.credentials(&git_config));
234 let mut fetch_options = FetchOptions::new();
235 fetch_options.update_fetchhead(false);
236 fetch_options.remote_callbacks(callbacks);
237 if git.checkout_ref.is_none() {
238 fetch_options.depth(1);
239 };
240 let mut repo_builder = RepoBuilder::new();
241 repo_builder.fetch_options(fetch_options);
242 let repo = repo_builder.clone(&url, dest_dir)?;
243
244 match &git.checkout_ref {
245 Some(checkout_ref) => {
246 let (object, _) = repo.revparse_ext(checkout_ref)?;
247 repo.checkout_tree(&object, None)?;
248 checkout_ref.clone()
249 }
250 None => {
251 let head = repo.head()?;
252 let commit = head.peel_to_commit()?;
253 commit.id().to_string()
254 }
255 }
256 };
257 remove_dir_all(dest_dir.join(".git")).map_err(FetchSrcError::CleanGitDir)?;
259 let hash = dest_dir.hash().await.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::http_client(config)?
271 .get(url.clone())
272 .send()
273 .await?
274 .error_for_status()?
275 .bytes()
276 .await?;
277 let hash = response.hash().await.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).await?;
309 dest_dir.hash().await.map_err(FetchSrcError::Hash)?
310 } else {
311 let mut file = fs::sync::open(path)?;
312 let mut buffer = Vec::new();
313 file.read_to_end(&mut buffer)
314 .map_err(|source| fs::FsError::Read {
315 path: path.to_path_buf(),
316 source,
317 })?;
318 let mime_type = infer::get(&buffer).map(|file_type| file_type.mime_type());
319 let file_name = path
320 .file_name()
321 .map(|os_str| os_str.to_string_lossy())
322 .unwrap_or(path.to_string_lossy())
323 .to_string();
324 operations::unpack::unpack(
325 mime_type,
326 file,
327 rock_source.unpack_dir.is_none(),
328 file_name,
329 dest_dir,
330 )
331 .await?;
332 path.hash().await.map_err(FetchSrcError::Hash)?
333 };
334 RemotePackageSourceMetadata {
335 hash,
336 source_url: RemotePackageSourceUrl::File { path: path.clone() },
337 }
338 }
339 };
340 Ok(metadata)
341}
342
343#[tracing::instrument(
344 name = "Fetching src.rock",
345 level = "info",
346 skip_all,
347 fields(package = fetch.package.to_string(),),
348)]
349async fn do_fetch_src_rock(
350 fetch: FetchSrcRock<'_>,
351) -> Result<RemotePackageSourceMetadata, FetchSrcRockError> {
352 let package = fetch.package;
353 let dest_dir = fetch.dest_dir;
354 let config = fetch.config;
355 let src_rock = operations::download_src_rock(package, config.server(), fetch.config).await?;
356 let hash = src_rock.bytes.hash().await?;
357 let cursor = Cursor::new(src_rock.bytes);
358 let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
359 operations::unpack::unpack(mime_type, cursor, true, src_rock.file_name, dest_dir).await?;
360 Ok(RemotePackageSourceMetadata {
361 hash,
362 source_url: RemotePackageSourceUrl::Url { url: src_rock.url },
363 })
364}