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