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